diff options
Diffstat (limited to 'devtools/processgamestats')
| -rw-r--r-- | devtools/processgamestats/StdAfx.cpp | 15 | ||||
| -rw-r--r-- | devtools/processgamestats/StdAfx.h | 26 | ||||
| -rw-r--r-- | devtools/processgamestats/base_gamestats_parse.h | 34 | ||||
| -rw-r--r-- | devtools/processgamestats/cs_gamestats.h | 45 | ||||
| -rw-r--r-- | devtools/processgamestats/cstrike_gamestats.db | 191 | ||||
| -rw-r--r-- | devtools/processgamestats/cstrike_gamestats_parse.cpp | 235 | ||||
| -rw-r--r-- | devtools/processgamestats/ep1_gamestats.db | 47 | ||||
| -rw-r--r-- | devtools/processgamestats/ep2_gamestats.db | 168 | ||||
| -rw-r--r-- | devtools/processgamestats/ep2_gamestats_parse.cpp | 606 | ||||
| -rw-r--r-- | devtools/processgamestats/portal_gamestats.db | 47 | ||||
| -rw-r--r-- | devtools/processgamestats/processgamestats.cpp | 982 | ||||
| -rw-r--r-- | devtools/processgamestats/processgamestats.vpc | 78 | ||||
| -rw-r--r-- | devtools/processgamestats/tf_gamestats.db | 227 | ||||
| -rw-r--r-- | devtools/processgamestats/tf_gamestats_parse.cpp | 480 |
14 files changed, 3181 insertions, 0 deletions
diff --git a/devtools/processgamestats/StdAfx.cpp b/devtools/processgamestats/StdAfx.cpp new file mode 100644 index 0000000..d9360a8 --- /dev/null +++ b/devtools/processgamestats/StdAfx.cpp @@ -0,0 +1,15 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// stdafx.cpp : source file that includes just the standard includes +// tagbuild.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/devtools/processgamestats/StdAfx.h b/devtools/processgamestats/StdAfx.h new file mode 100644 index 0000000..8b6ae51 --- /dev/null +++ b/devtools/processgamestats/StdAfx.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#if !defined(AFX_STDAFX_H__BC019BF6_D3B1_4EA1_AA0E_044EEC919C6D__INCLUDED_) +#define AFX_STDAFX_H__BC019BF6_D3B1_4EA1_AA0E_044EEC919C6D__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + + +// TODO: reference additional headers your program requires here + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_STDAFX_H__BC019BF6_D3B1_4EA1_AA0E_044EEC919C6D__INCLUDED_) diff --git a/devtools/processgamestats/base_gamestats_parse.h b/devtools/processgamestats/base_gamestats_parse.h new file mode 100644 index 0000000..6da452a --- /dev/null +++ b/devtools/processgamestats/base_gamestats_parse.h @@ -0,0 +1,34 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef BASE_GAMESTATS_PARSE_H +#define BASE_GAMESTATS_PARSE_H +#ifdef _WIN32 +#pragma once +#endif + +enum CustomDataReturnCode +{ + CUSTOMDATA_NONE = 0, + CUSTOMDATA_FAILED, + CUSTOMDATA_SUCCESS +}; + +class IMySQL; + +struct ParseContext_t +{ + char const *file; + char const *gamename; + bool describeonly; + IMySQL *mysql; + int skipcount; + bool bCustomDirectoryNotMade; +}; + +void ProcessPerfData( IMySQL *pSQL, time_t fileTime, char const *pGameName, char const *pPerfString ); + +#endif // BASE_GAMESTATS_PARSE_H diff --git a/devtools/processgamestats/cs_gamestats.h b/devtools/processgamestats/cs_gamestats.h new file mode 100644 index 0000000..25fd1ef --- /dev/null +++ b/devtools/processgamestats/cs_gamestats.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The cs game stats header +// +// $NoKeywords: $ +//=============================================================================// + +#define CS_STATS_BLOB_VERSION 3 + +extern const char *pValidStatLevels[]; + +#define CS_NUM_LEVELS 18 +#define WEAPON_MAX 34 + +int GetCSLevelIndex( const char *pLevelName ); + +typedef struct +{ + char szGameName[8]; + byte iVersion; + char szMapName[32]; + char ipAddr[4]; + short port; + int serverid; +} gamestats_header_t; + +typedef struct +{ + gamestats_header_t header; + short iMinutesPlayed; + + short iTerroristVictories[CS_NUM_LEVELS]; + short iCounterTVictories[CS_NUM_LEVELS]; + short iBlackMarketPurchases[WEAPON_MAX]; + + short iAutoBuyPurchases; + short iReBuyPurchases; + short iAutoBuyM4A1Purchases; + short iAutoBuyAK47Purchases; + short iAutoBuyFamasPurchases; + short iAutoBuyGalilPurchases; + short iAutoBuyVestHelmPurchases; + short iAutoBuyVestPurchases; + +} cs_gamestats_t; diff --git a/devtools/processgamestats/cstrike_gamestats.db b/devtools/processgamestats/cstrike_gamestats.db new file mode 100644 index 0000000..30210d3 --- /dev/null +++ b/devtools/processgamestats/cstrike_gamestats.db @@ -0,0 +1,191 @@ +create table weapons +( + WeaponID INT, + PRIMARY KEY( WeaponID ), + Count INT, +) TYPE=MyISAM; + +create table maps +( + MapName CHAR(16), + PRIMARY KEY( MapName ), + TerroristWins INT, + CTWins INT, +) TYPE=MyISAM; + +create table autobuy +( + Purchase CHAR(16), + PRIMARY KEY( Purchase ), + Count INT, +) TYPE=MyISAM; + +create table purchases_pricing +( + snapshot CHAR(16), + PRIMARY KEY( snapshot ), + dt2 DATETIME, + weapon_id INT, + purchases INT, + current_price INT, + projected_price INT, + +) TYPE=MyISAM; + +create table snapshot_counter +( + counter INT, + PRIMARY KEY( counter ), + +) TYPE=MyISAM; + +create table weapon_info +( + WeaponID INT, + PRIMARY KEY( WeaponID ), + side ENUM('Terrorists', 'Counter-Terrorists', 'Both'), + fullname CHAR( 32 ), + default_price INT, + current_price INT, +) TYPE=MyISAM; + + + +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 1, 3, "228 COMPACT", 600, 600 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 2, 3, "9X19MM SIDEARM", 400, 400 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 16, 3, "K&M .45 TACTICAL", 500, 500 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 25, 3, "NIGHT HAWK .50C", 650, 650 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 11, 2, "ES FIVE-SEVEN", 750, 750 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 10, 1, ".40 DUAL ELITES", 800, 800 ); + +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 20, 3, "LEONE 12 GAUGE SUPER", 1700, 1700 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 5, 3, "LEONE YG1265 AUTO SHOTGUN", 3000, 3000 ); + +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 22, 2, "SCHMIDT MACHINE PISTOL", 1250, 1250 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 18, 3, "K&M SUB-MACHINE GUN", 1500, 1500 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 12, 3, "K&M UMP45", 1700, 1700 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 29, 3, "ES C90", 2350, 2350 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 7, 1, "INGRAM MAC-10", 1400, 1400 ); + +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 15, 2, "CLARION 5.56", 2250, 2250 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 3, 3, "SCHMIDT SCOUT", 2750, 2750 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 21, 2, "MAVERICK M4A1 CARBINE", 3100, 3100 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 8, 2, "BULLPUP", 3500, 3500 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 13, 1, "KRIEG 550 COMMANDO", 4200, 4200 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 26, 1, "KRIEG 552", 3500, 3500 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 17, 3, "MAGNUM SNIPER RIFLE", 4750, 4750 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 14, 1, "IDF DEFENDER", 2000, 2000 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 27, 1, "CV-47", 2500, 2500 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 23, 2, "D3/AU-1", 5000, 5000 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 19, 3, "M249", 5750, 5750 ); + +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 31, 3, "KEVLAR", 650, 650 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 32, 3, "KEVLAR+HELMET", 1000, 1000 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 24, 3, "FLASHBANG", 200, 200 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 4, 3, "HE GRENADE", 300, 300 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 9, 3, "SMOKE GRENADE", 300, 300 ); +Insert into weapon_info ( WeaponID, side, fullname, default_price, current_price ) values ( 33, 3, "NIGHTVISION", 1250, 1250 ); + + + +# update weapons set Count=Count+450 where WeaponID = 0; +# Insert into weapons (WeaponID,Count) values ( 1, 0 ); + +# update maps set TerroristWins=0, CTWins=0 where MapName = "cs_assault"; + +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_assault", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_compound", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_havana", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_italy", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_militia", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "cs_office", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_aztec", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_cbble", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_chateau", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_dust2", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_dust", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_inferno", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_nuke", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_piranesi", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_port", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_prodigy", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_tides", 0, 0 ); +# Insert into maps (MapName,TerroristWins, CTWins) values ( "de_train", 0, 0 ); + + +//Autobuy +Insert into autobuy (Purchase, Count) values ( "Auto-Buy", 0 ); +Insert into autobuy (Purchase, Count) values ( "Re-Buy", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: M4A1", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: AK47", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: Famas", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: Galil", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: Suit", 0 ); +Insert into autobuy (Purchase, Count) values ( "Auto-Buy: Kev", 0 ); + +update autobuy set Count=0 where Purchase = "Auto-Buy"; +update autobuy set Count=0 where Purchase = "Re-Buy"; +update autobuy set Count=0 where Purchase = "Auto-Buy: M4A1"; +update autobuy set Count=0 where Purchase = "Auto-Buy: AK47"; +update autobuy set Count=0 where Purchase = "Auto-Buy: Famas"; +update autobuy set Count=0 where Purchase = "Auto-Buy: Galil"; +update autobuy set Count=0 where Purchase = "Auto-Buy: Suit"; +update autobuy set Count=0 where Purchase = "Auto-Buy: Kev"; + + + +//Copy and Paste into the SQL terminal to reset everything + +update weapons set Count=0 where WeaponID = 1; +update weapons set Count=0 where WeaponID = 2; +update weapons set Count=0 where WeaponID = 3; +update weapons set Count=0 where WeaponID = 4; +update weapons set Count=0 where WeaponID = 5; +update weapons set Count=0 where WeaponID = 6; +update weapons set Count=0 where WeaponID = 7; +update weapons set Count=0 where WeaponID = 8; +update weapons set Count=0 where WeaponID = 9; +update weapons set Count=0 where WeaponID = 10; +update weapons set Count=0 where WeaponID = 11; +update weapons set Count=0 where WeaponID = 12; +update weapons set Count=0 where WeaponID = 13; +update weapons set Count=0 where WeaponID = 14; +update weapons set Count=0 where WeaponID = 15; +update weapons set Count=0 where WeaponID = 16; +update weapons set Count=0 where WeaponID = 17; +update weapons set Count=0 where WeaponID = 18; +update weapons set Count=0 where WeaponID = 19; +update weapons set Count=0 where WeaponID = 20; +update weapons set Count=0 where WeaponID = 21; +update weapons set Count=0 where WeaponID = 22; +update weapons set Count=0 where WeaponID = 23; +update weapons set Count=0 where WeaponID = 24; +update weapons set Count=0 where WeaponID = 25; +update weapons set Count=0 where WeaponID = 26; +update weapons set Count=0 where WeaponID = 27; +update weapons set Count=0 where WeaponID = 28; +update weapons set Count=0 where WeaponID = 29; +update weapons set Count=0 where WeaponID = 30; +update weapons set Count=0 where WeaponID = 31; +update weapons set Count=0 where WeaponID = 32; +update weapons set Count=0 where WeaponID = 33; + +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_assault"; +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_compound"; +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_havana"; +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_italy"; +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_militia"; +update maps set TerroristWins=0, CTWins=0 where MapName = "cs_office"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_aztec"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_cbble"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_chateau"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_dust2"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_dust"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_inferno"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_nuke"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_piranesi"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_port"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_prodigy"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_tides"; +update maps set TerroristWins=0, CTWins=0 where MapName = "de_train"; + diff --git a/devtools/processgamestats/cstrike_gamestats_parse.cpp b/devtools/processgamestats/cstrike_gamestats_parse.cpp new file mode 100644 index 0000000..6180279 --- /dev/null +++ b/devtools/processgamestats/cstrike_gamestats_parse.cpp @@ -0,0 +1,235 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// +// + +#include "stdafx.h" +#include <stdio.h> +#include <process.h> +#include <string.h> +#include <windows.h> +#include <sys/stat.h> + +#include "interface.h" +#include "imysqlwrapper.h" +#include "tier1/utlvector.h" +#include "tier1/utlbuffer.h" +#include "tier1/utlsymbol.h" +#include "tier1/utlstring.h" +#include "tier1/utldict.h" +#include "tier2/tier2.h" +#include "filesystem.h" + +#include "cbase.h" +#include "gamestats.h" +#include "cs_gamestats.h" +#include "base_gamestats_parse.h" + +extern CUtlDict< int, unsigned short > g_mapOrder; + +const char *pValidStatLevels[] = +{ + "cs_assault", + "cs_compound", + "cs_havana", + "cs_italy", + "cs_militia", + "cs_office", + "de_aztec", + "de_cbble", + "de_chateau", + "de_dust2", + "de_dust", + "de_inferno", + "de_nuke", + "de_piranesi", + "de_port", + "de_prodigy", + "de_tides", + "de_train", +}; + +static const char * s_WeaponAliasInfo[] = +{ + "none", // WEAPON_NONE + "p228", // WEAPON_P228 + "glock", // WEAPON_GLOCK // old glock + "scout", // WEAPON_SCOUT + "hegren", // WEAPON_HEGRENADE + "xm1014", // WEAPON_XM1014 // auto shotgun + "c4", // WEAPON_C4 + "mac10", // WEAPON_MAC10 // T only + "aug", // WEAPON_AUG + "sgren", // WEAPON_SMOKEGRENADE + "elite", // WEAPON_ELITE + "fiveseven",// WEAPON_FIVESEVEN + "ump45", // WEAPON_UMP45 + "sg550", // WEAPON_SG550 // auto-sniper + "galil", // WEAPON_GALIL + "famas", // WEAPON_FAMAS // CT cheap m4a1 + "usp", // WEAPON_USP + "awp", // WEAPON_AWP + "mp5navy", // WEAPON_MP5N + "m249", // WEAPON_M249 // big machinegun + "m3", // WEAPON_M3 // cheap shotgun + "m4a1", // WEAPON_M4A1 + "tmp", // WEAPON_TMP + "g3sg1", // WEAPON_G3SG1 // T auto-sniper + "flash", // WEAPON_FLASHBANG + "deagle", // WEAPON_DEAGLE + "sg552", // WEAPON_SG552 // T aug equivalent + "ak47", // WEAPON_AK47 + "knife", // WEAPON_KNIFE + "p90", // WEAPON_P90 + "shield", // WEAPON_SHIELDGUN + "kevlar", + "assaultsuit", + "nightvision", + NULL, // WEAPON_NONE +}; + +void DescribeData( cs_gamestats_t &stats ) +{ + Msg( " Blob version: %d\n", stats.header.iVersion ); + Msg( " Server Uptime: %d\n", stats.iMinutesPlayed ); + + for ( int i = 0; i < CS_NUM_LEVELS; i++ ) + { + Msg( "%s - Terrorists Wins: %d | Counter-Terrorists Wins: %d\n", pValidStatLevels[i], stats.iTerroristVictories[i], stats.iCounterTVictories[i] ); + } + + for ( int i = 0; i < WEAPON_MAX; i++ ) + { + Msg( "%s was purchased %d time(s)\n", s_WeaponAliasInfo[i], stats.iBlackMarketPurchases[i] ); + } + + + char q[ 512 ]; + + Q_snprintf( q, sizeof( q ), "Auto-Buy = %d\n", stats.iAutoBuyPurchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Re-Buy = %d\n", stats.iReBuyPurchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: M4A1 = %d\n", stats.iAutoBuyM4A1Purchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: AK47 = %d\n", stats.iAutoBuyAK47Purchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: Famas = %d\n", stats.iAutoBuyFamasPurchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: Galil = %d\n", stats.iAutoBuyGalilPurchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: Suit = %d\n", stats.iAutoBuyVestHelmPurchases ); + Msg( q ); + + Q_snprintf( q, sizeof( q ), "Auto-Buy: Kev = %d\n", stats.iAutoBuyVestPurchases ); + Msg( q ); +} + +int CS_ParseCustomGameStatsData( ParseContext_t *ctx ) +{ + if ( g_pFullFileSystem == NULL ) + return CUSTOMDATA_FAILED; + + FileHandle_t FileHandle = g_pFullFileSystem->Open( ctx->file, "rb" ); + + if ( !FileHandle ) + { + return CUSTOMDATA_FAILED; + } + + if ( ctx->mysql == NULL && ctx->describeonly == false ) + return CUSTOMDATA_FAILED; + + char q[ 512 ]; + cs_gamestats_t stats; + g_pFullFileSystem->Read( &stats, sizeof( cs_gamestats_t ), FileHandle ); + + if ( Q_stricmp( stats.header.szGameName, "cstrike" ) ) + return CUSTOMDATA_FAILED; + + if ( stats.header.iVersion != CS_STATS_BLOB_VERSION ) + { + Msg( "Error: Incorrect Blob Version! Got: %d - Expected: %d\n", stats.header.iVersion, CS_STATS_BLOB_VERSION ); + return CUSTOMDATA_FAILED; + } + + if ( ctx->describeonly == true ) + { + DescribeData( stats ); + return CUSTOMDATA_SUCCESS; + } + + //Do maps first + for ( int i = 0; i < CS_NUM_LEVELS; i++ ) + { + Q_snprintf( q, sizeof( q ), "update maps set TerroristWins=TerroristWins+%d, CTWins=CTWins+%d where MapName = \"%s\";", stats.iTerroristVictories[i], stats.iCounterTVictories[i], pValidStatLevels[i] ); + + int retcode = ctx->mysql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query:\n %s\n failed\n", q ); + return CUSTOMDATA_FAILED; + } + } + + //Now do all weapons + for ( int i = 0; i < WEAPON_MAX; i++ ) + { + int iWeaponID = i; + + //HACKHACK: Fix up incorrect data for the smoke grenades. + if ( i == 0 && stats.iBlackMarketPurchases[i] != 0 ) + { + iWeaponID = 9; + } + + Q_snprintf( q, sizeof( q ), "update weapons set Count=Count+%d where WeaponID = %d;", stats.iBlackMarketPurchases[i], iWeaponID ); + + int retcode = ctx->mysql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query:\n %s\n failed\n", q ); + return CUSTOMDATA_FAILED; + } + } + + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy\";", stats.iAutoBuyPurchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Re-Buy\";", stats.iReBuyPurchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: M4A1\";", stats.iAutoBuyM4A1Purchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: AK47\";", stats.iAutoBuyAK47Purchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: Famas\";", stats.iAutoBuyFamasPurchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: Galil\";", stats.iAutoBuyGalilPurchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: Suit\";", stats.iAutoBuyVestHelmPurchases ); + ctx->mysql->Execute( q ); + + Q_snprintf( q, sizeof( q ), "update autobuy set Count=Count+%d where Purchase = \"Auto-Buy: Kev\";", stats.iAutoBuyVestPurchases ); + ctx->mysql->Execute( q ); + + + + return CUSTOMDATA_SUCCESS; +} diff --git a/devtools/processgamestats/ep1_gamestats.db b/devtools/processgamestats/ep1_gamestats.db new file mode 100644 index 0000000..a8b05dd --- /dev/null +++ b/devtools/processgamestats/ep1_gamestats.db @@ -0,0 +1,47 @@ +# create database gamestats; +# use gamestats; + +create table ep1 +( + UserID CHAR(16), + PRIMARY KEY( UserID ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + hl2_chapter TINYINT, + SecondsToCompleteGame INT, # Non-zero if user has completed game + HighestMap CHAR(16), + DXLevel INT, + Deaths INT, +) TYPE=MyISAM; + +create table ep1_maps +( + UserID CHAR(16), + MapName CHAR(16), + PRIMARY KEY( UserID,MapName ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + Deaths INT, +) TYPE=MyISAM;
\ No newline at end of file diff --git a/devtools/processgamestats/ep2_gamestats.db b/devtools/processgamestats/ep2_gamestats.db new file mode 100644 index 0000000..9002bf3 --- /dev/null +++ b/devtools/processgamestats/ep2_gamestats.db @@ -0,0 +1,168 @@ +create database gamestats_ep2; +use gamestats_ep2; + +create table ep2 +( + UserID CHAR(16), + PRIMARY KEY( UserID ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Tag CHAR(8), + KEY( Tag ), + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + hl2_chapter TINYINT, + SecondsToCompleteGame INT, # Non-zero if user has completed game + HighestMap CHAR(20), + DXLevel INT, + Deaths INT +) TYPE=MyISAM; + +create table ep2_maps +( + UserID CHAR(16), + MapName CHAR(20), + PRIMARY KEY( UserID,MapName ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Tag CHAR(8), + KEY( Tag ), + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + Deaths INT +) TYPE=MyISAM; + +create table ep2_entities +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + Entity CHAR(32), + PRIMARY KEY( UserID,Tag,MapName,Entity ), + LastUpdate DATETIME, + KEY( LastUpdate ), + BodyCount INT, + KilledPlayer INT +) TYPE=MyISAM; + +create table ep2_deaths +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + LastUpdate DATETIME, + KEY( LastUpdate ), + DeathIndex INT, + X SMALLINT, + Y SMALLINT, + Z SMALLINT, + PRIMARY KEY ( UserID, Tag, MapName, X, Y, Z ) +) TYPE=MyISAM; + +create table ep2_weapons +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + Weapon CHAR(32), + PRIMARY KEY( UserID,Tag, MapName,Weapon ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Shots INT, + Hits INT, + Damage DOUBLE +) TYPE=MyISAM; + +create table ep2_saves +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + LastUpdate DATETIME, + KEY( LastUpdate ), + FIRSTDEATH INT, # index into ep2_deaths + NUMDEATHS INT, + X SMALLINT, + Y SMALLINT, + Z SMALLINT, + HEALTH SMALLINT, + SAVETYPE TINYINT, # 0 unknown, 1 autosave, 2 user save (quick or other) + PRIMARY KEY( UseriD, Tag, MapName, FirstDeath, NumDeaths ) +) TYPE=MyISAM; + +create table ep2_counters +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + PRIMARY KEY( UserID,Tag,MapName ), + LastUpdate DATETIME, + KEY( LastUpdate ), + CRATESSMASHED INT, + OBJECTSPUNTED INT, + VEHICULARHOMICIDES INT, + DISTANCE_INVEHICLE BIGINT, + DISTANCE_ONFOOT BIGINT, + DISTANCE_ONFOOTSPRINTING BIGINT, + FALLINGDEATHS INT, + VEHICLE_OVERTURNED INT, + LOADGAME_STILLALIVE INT, + LOADS INT, + SAVES INT, + GODMODES INT, + NOCLIPS INT, + + DAMAGETAKEN DOUBLE +) TYPE=MyISAM; + +create table ep2_generic +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + StatName CHAR(16), + PRIMARY KEY( UserID,Tag, MapName,StatName ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Count INT, + Value DOUBLE, + X SMALLINT, + Y SMALLINT, + Z SMALLINT +) TYPE=MyISAM;
\ No newline at end of file diff --git a/devtools/processgamestats/ep2_gamestats_parse.cpp b/devtools/processgamestats/ep2_gamestats_parse.cpp new file mode 100644 index 0000000..0e1b67b --- /dev/null +++ b/devtools/processgamestats/ep2_gamestats_parse.cpp @@ -0,0 +1,606 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "stdafx.h" +#include <stdio.h> +#include <process.h> +#include <string.h> +#include <windows.h> +#include <sys/stat.h> +#include <time.h> +#include <string> + + +#include "interface.h" +#include "imysqlwrapper.h" +#include "tier1/utlvector.h" +#include "tier1/utlbuffer.h" +#include "tier1/utlsymbol.h" +#include "tier1/utlstring.h" +#include "tier1/utldict.h" +#include "tier2/tier2.h" +#include "filesystem.h" + +#include "cbase.h" +#include "gamestats.h" +#include "episodic/ep2_gamestats.h" +#include "base_gamestats_parse.h" + +void v_escape_string( std::string& s ); + +// EP2 custom data blob parsing stuff + +static Ep2LevelStats_t *g_pCurrentMap; +static CUtlDict<Ep2LevelStats_t, unsigned short> g_dictMapStats; + +extern CUtlDict< int, unsigned short > g_mapOrder; + +void DescribeData( BasicGameStats_t &stats, const char *szStatsFileUserID, int iStatsFileVersion ); +void InsertData( CUtlDict< int, unsigned short >& mapOrder, IMySQL *sql, BasicGameStats_t &gs, const char *szStatsFileUserID, int iStatsFileVersion, const char *gamename, char const *tag = NULL ); + +struct CounterInfo_t +{ + int type; + char const *counterName; +}; + +static CounterInfo_t g_IntCounterNames[ ] = +{ + { Ep2LevelStats_t::COUNTER_CRATESSMASHED, "Crates Smashed" }, + { Ep2LevelStats_t::COUNTER_OBJECTSPUNTED, "Objects Punted" }, + { Ep2LevelStats_t::COUNTER_VEHICULARHOMICIDES, "Vehicular Homicides" }, + { Ep2LevelStats_t::COUNTER_DISTANCE_INVEHICLE, "Distance in Vehicles (inches)" }, + { Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOT, "Distance on Foot (inches)" }, + { Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOTSPRINTING, "Distance on Foot Sprint (inches)" }, + { Ep2LevelStats_t::COUNTER_FALLINGDEATHS, "Falling Death" }, + { Ep2LevelStats_t::COUNTER_VEHICLE_OVERTURNED, "Flipped Vehicles" }, + { Ep2LevelStats_t::COUNTER_LOADGAME_STILLALIVE, "Loadgame while player alive" }, + { Ep2LevelStats_t::COUNTER_LOADS, "Number of saves (w/autosaves)" }, + { Ep2LevelStats_t::COUNTER_SAVES, "Number of game loads" }, + { Ep2LevelStats_t::COUNTER_GODMODES, "Times entered God mode" }, + { Ep2LevelStats_t::COUNTER_NOCLIPS, "Times entered NoClip mode" }, +}; + +static CounterInfo_t g_FloatCounterNames[ ] = +{ + { Ep2LevelStats_t::COUNTER_DAMAGETAKEN, "Damage Taken" }, +}; + +char const *SaveType( int type ) +{ + switch ( type ) + { + default: + case Ep2LevelStats_t::SaveGameInfoRecord2_t::TYPE_UNKNOWN: + break; + case Ep2LevelStats_t::SaveGameInfoRecord2_t::TYPE_AUTOSAVE: + return "Auto Save"; + case Ep2LevelStats_t::SaveGameInfoRecord2_t::TYPE_USERSAVE: + return "User Save"; + } + return "Unknown"; +} + +#define INCHES_TO_MILES ( 1.0 / ( 5280.0 * 12.0 ) ) +#define INCHES_TO_FEET ( 1.0 / 12.0 ) + +void DescribeEp2Stats() +{ + int i; + int iMap; + for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) ) + { + // Get the current map. + Ep2LevelStats_t *pCurrentMap = &g_dictMapStats[iMap]; + + Msg( "map version[ %d ], user tag[ %s ]\n", pCurrentMap->m_Tag.m_nMapVersion, pCurrentMap->m_Tag.m_szTagText ); + + Msg( " --- %s ------\n %d deaths\n", pCurrentMap->m_Header.m_szMapName, pCurrentMap->m_aPlayerDeaths.Count() ); + for ( i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); ++i ) + { + Msg( " @ ( %d, %d, %d )\n", + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ] ); + } + + Msg( " -- Weapon Stats --\n" ); + + CUtlDict< Ep2LevelStats_t::WeaponLump_t, int > &wdict = pCurrentMap->m_dictWeapons; + for ( i = wdict.First(); i != wdict.InvalidIndex(); i = wdict.Next( i ) ) + { + const Ep2LevelStats_t::WeaponLump_t &lump = wdict[ i ]; + double acc = 0.0f; + if ( lump.m_nShots > 0 ) + { + acc = 100.0f * (double)lump.m_nHits / (double)lump.m_nShots; + } + Msg( " %32.32s: shots %5d hits %5d damage %8.2f [accuracy %8.2f %%]\n", wdict.GetElementName( i ), lump.m_nShots, lump.m_nHits, lump.m_flDamageInflicted, acc ); + } + Msg( " -- NPC Stats -- \n" ); + + CUtlDict< Ep2LevelStats_t::EntityDeathsLump_t, int > &dict = pCurrentMap->m_dictEntityDeaths; + for ( i = dict.First(); i != dict.InvalidIndex(); i = dict.Next( i ) ) + { + const Ep2LevelStats_t::EntityDeathsLump_t &lump = dict[ i ]; + Msg( " %32.32s: bodycount %5d killedplayer %3d\n", dict.GetElementName( i ), lump.m_nBodyCount, lump.m_nKilledPlayer ); + } + + for ( i = 0; i < Ep2LevelStats_t::NUM_INTCOUNTER_TYPES; ++i ) + { + if ( i == Ep2LevelStats_t::COUNTER_DISTANCE_INVEHICLE || + i == Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOT || + i == Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOTSPRINTING ) + { + Msg( " %32.32s: %8I64u [%8.3f feet] [%8.3f miles]\n", g_IntCounterNames[ i ].counterName, pCurrentMap->m_IntCounters[ i ], INCHES_TO_FEET * (double)pCurrentMap->m_IntCounters[ i ], INCHES_TO_MILES * (double)pCurrentMap->m_IntCounters[ i ] ); + } + else + { + Msg( " %32.32s: %8I64u\n", g_IntCounterNames[ i ].counterName, pCurrentMap->m_IntCounters[ i ] ); + } + } + for ( i = 0; i < Ep2LevelStats_t::NUM_FLOATCOUNTER_TYPES; ++i ) + { + Msg( " %32.32s: %8.2f\n", g_FloatCounterNames[ i ].counterName, pCurrentMap->m_FloatCounters[ i ] ); + } + + Ep2LevelStats_t::SaveGameInfo_t *sg = &pCurrentMap->m_SaveGameInfo; + Msg( " -- Save Game --\n" ); + time_t t = (time_t)sg->m_nCurrentSaveFileTime; + struct tm *timestamp; + timestamp = localtime( &t ); + if ( t == (time_t)0 ) + { + Msg( " No save file\n" ); + } + else + { + Msg( " Current save %s file time %s\n", sg->m_sCurrentSaveFile.String(), asctime( timestamp ) ); + } + for ( i = 0; i < sg->m_Records.Count(); ++i ) + { + const Ep2LevelStats_t::SaveGameInfoRecord2_t &rec = sg->m_Records[ i ]; + Msg( " %3d deaths, saved at (%5d %5d %5d) with health %3d %s\n", + rec.m_nNumDeaths, + (int)rec.m_nSavePos[ 0 ], + (int)rec.m_nSavePos[ 1 ], + (int)rec.m_nSavePos[ 2 ], + (int)rec.m_nSaveHealth, + SaveType( (int)rec.m_SaveType ) ); + } + + Msg( " -- Generic Stats --\n" ); + + CUtlDict< Ep2LevelStats_t::GenericStatsLump_t, int > &gdict = pCurrentMap->m_dictGeneric; + for ( i = gdict.First(); i != gdict.InvalidIndex(); i = gdict.Next( i ) ) + { + const Ep2LevelStats_t::GenericStatsLump_t &lump = gdict[ i ]; + Msg( " %32.32s: count %u total %f @[%d, %d, %d]\n", gdict.GetElementName( i ), lump.m_unCount, lump.m_flCurrentValue, lump.m_Pos[ 0 ], lump.m_Pos[ 1 ], lump.m_Pos[ 2 ] ); + } + + Msg( "\n" ); + } +} + +void InsertCustomData( CUtlDict< int, unsigned short >& mapOrder, IMySQL *sql, const char *szStatsFileUserID, int iStatsFileVersion, const char *gamename ) +{ + if ( !sql ) + return; + + char q[ 1024 ]; + char counternames[ 512 ]; + + std::string userid; + userid = szStatsFileUserID; + v_escape_string( userid ); + + // Deal with the maps + int i; + int iMap; + for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) ) + { + // Get the current map. + Ep2LevelStats_t *pCurrentMap = &g_dictMapStats[iMap]; + + char const *pszMapName = g_dictMapStats.GetElementName( iMap ); + std::string mapname; + mapname = pszMapName; + v_escape_string( mapname ); + + std::string tag; + tag = pCurrentMap->m_Tag.m_szTagText; + v_escape_string( tag ); + + int mapversion = pCurrentMap->m_Tag.m_nMapVersion; + +#if 1 + int slot = mapOrder.Find( pszMapName ); + if ( slot == mapOrder.InvalidIndex() ) + { + if ( Q_stricmp( pszMapName, "devtest" ) ) + continue; + } +#endif + + // Deal with deaths + for ( i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); ++i ) + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s_deaths (UserID,Tag,LastUpdate,MapName,MapVersion,DeathIndex,X,Y,Z) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,%d,%d,%d,%d);", + gamename, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + i, + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ] ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + + // Deal with weapon data + CUtlDict< Ep2LevelStats_t::WeaponLump_t, int > &wdict = pCurrentMap->m_dictWeapons; + for ( i = wdict.First(); i != wdict.InvalidIndex(); i = wdict.Next( i ) ) + { + const Ep2LevelStats_t::WeaponLump_t &lump = wdict[ i ]; + + std::string wname = wdict.GetElementName( i ); + v_escape_string( wname ); + + Q_snprintf( q, sizeof( q ), "REPLACE into %s_weapons (UserID,Tag,LastUpdate,MapName,MapVersion,Weapon,Shots,Hits,Damage) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,\"%-.32s\",%d,%d,%g);", + gamename, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + wname.c_str(), + lump.m_nShots, + lump.m_nHits, + lump.m_flDamageInflicted + ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + + CUtlDict< Ep2LevelStats_t::EntityDeathsLump_t, int > &dict = pCurrentMap->m_dictEntityDeaths; + for ( i = dict.First(); i != dict.InvalidIndex(); i = dict.Next( i ) ) + { + const Ep2LevelStats_t::EntityDeathsLump_t &lump = dict[ i ]; + + std::string ename = dict.GetElementName( i ); + v_escape_string( ename ); + + Q_snprintf( q, sizeof( q ), "REPLACE into %s_entities (UserID,Tag,LastUpdate,MapName,MapVersion,Entity,BodyCount,KilledPlayer) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,\"%-.32s\",%d,%d);", + gamename, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + ename.c_str(), + lump.m_nBodyCount, + lump.m_nKilledPlayer + ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + + // Counters + Q_snprintf( counternames, sizeof( counternames ), + "CRATESSMASHED,"\ + "OBJECTSPUNTED,"\ + "VEHICULARHOMICIDES,"\ + "DISTANCE_INVEHICLE,"\ + "DISTANCE_ONFOOT,"\ + "DISTANCE_ONFOOTSPRINTING,"\ + "FALLINGDEATHS,"\ + "VEHICLE_OVERTURNED,"\ + "LOADGAME_STILLALIVE,"\ + "LOADS,"\ + "SAVES,"\ + "GODMODES,"\ + "NOCLIPS,"\ + "DAMAGETAKEN" ); + Q_snprintf( q, sizeof( q ), "REPLACE into %s_counters (UserID,Tag,LastUpdate,MapName,MapVersion,%s) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%I64u,"\ + "%I64u,"\ + "%I64u,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%u,"\ + "%f);", + gamename, + counternames, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_CRATESSMASHED ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_OBJECTSPUNTED ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICULARHOMICIDES ], + pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_INVEHICLE ], + pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOT ], + pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOTSPRINTING ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_FALLINGDEATHS ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICLE_OVERTURNED ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADGAME_STILLALIVE ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADS ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_SAVES ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_GODMODES ], + (uint32)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ], + (double)pCurrentMap->m_FloatCounters[ Ep2LevelStats_t::COUNTER_DAMAGETAKEN ] + ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + + Ep2LevelStats_t::SaveGameInfo_t *sg = &pCurrentMap->m_SaveGameInfo; + /* + Msg( " -- Save Game --\n" ); + time_t t = (time_t)sg->m_nCurrentSaveFileTime; + struct tm *timestamp; + timestamp = localtime( &t ); + if ( t == (time_t)0 ) + { + Msg( " No save file\n" ); + } + else + { + Msg( " Current save %s file time %s\n", sg->m_sCurrentSaveFile.String(), asctime( timestamp ) ); + } + */ + for ( i = 0; i < sg->m_Records.Count(); ++i ) + { + const Ep2LevelStats_t::SaveGameInfoRecord2_t &rec = sg->m_Records[ i ]; + + Q_snprintf( q, sizeof( q ), "REPLACE into %s_saves (UserID,Tag,LastUpdate,MapName,MapVersion,FirstDeath,NumDeaths,X,Y,Z,Health,SaveType) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,%d,%d,%d,%d,%d,%d,%d);", + gamename, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + rec.m_nFirstDeathIndex, + rec.m_nNumDeaths, + (int)rec.m_nSavePos[ 0 ], + (int)rec.m_nSavePos[ 1 ], + (int)rec.m_nSavePos[ 2 ], + (int)rec.m_nSaveHealth, + (int)rec.m_SaveType + ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + + + // Deal with generic stats data + CUtlDict< Ep2LevelStats_t::GenericStatsLump_t, int > &gdict = pCurrentMap->m_dictGeneric; + for ( i = gdict.First(); i != gdict.InvalidIndex(); i = gdict.Next( i ) ) + { + const Ep2LevelStats_t::GenericStatsLump_t &lump = gdict[ i ]; + + std::string statname = gdict.GetElementName( i ); + v_escape_string( statname ); + + Q_snprintf( q, sizeof( q ), "REPLACE into %s_generic (UserID,Tag,LastUpdate,MapName,MapVersion,StatName,Count,Value,X,Y,Z) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,\"%-.16s\",%d,%f,%d,%d,%d);", + gamename, + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + statname.c_str(), + lump.m_unCount, + lump.m_flCurrentValue, + lump.m_Pos[ 0 ], + lump.m_Pos[ 1 ], + lump.m_Pos[ 2 ] + ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *szMapName - +// Output : Ep2LevelStats_t +//----------------------------------------------------------------------------- +Ep2LevelStats_t *FindOrAddMapStats( const char *szMapName ) +{ + int iMap = g_dictMapStats.Find( szMapName ); + if( iMap == g_dictMapStats.InvalidIndex() ) + { + iMap = g_dictMapStats.Insert( szMapName ); + } + + return &g_dictMapStats[iMap]; +} + +int GetMaxLumpCount() +{ + return EP2_MAX_LUMP_COUNT; +} + +void LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer, char *tag, size_t tagsize ) +{ + tag[ 0 ] = 0; + Ep2LevelStats_t::LoadData( g_dictMapStats, LoadBuffer ); + if ( g_dictMapStats.Count() > 0 ) + { + Q_strncpy( tag, g_dictMapStats[ g_dictMapStats.First() ].m_Tag.m_szTagText, tagsize ); + } + + int i; + int iMap; + for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) ) + { + // Get the current map. + Ep2LevelStats_t *pCurrentMap = &g_dictMapStats[iMap]; + CUtlDict< Ep2LevelStats_t::WeaponLump_t, int > &wdict = pCurrentMap->m_dictWeapons; + for ( i = wdict.First(); i != wdict.InvalidIndex(); i = wdict.Next( i ) ) + { + Ep2LevelStats_t::WeaponLump_t &lump = wdict[ i ]; + // Bogus #, some kind of damage scaling? + if ( lump.m_flDamageInflicted > 50000 ) + { + lump.m_flDamageInflicted = 0.0f; + lump.m_nHits = 0; + lump.m_nShots = 0; + } + } + + if ( (int64)pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ] < 0 ) + { + pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ] = 0; + } + } +} + +bool Ep2_ParseCurrentUserID( char const *pchDataFile, char *pchUserID, size_t bufsize, time_t &modifiedtime ) +{ + FILE *fp = fopen( pchDataFile, "rb" ); + if ( fp ) + { + CUtlBuffer statsBuffer; + + struct _stat sb; + _stat( pchDataFile, &sb ); + + // Msg( "Processing %s\n", ctx->file ); + int nBytesToRead = min( sb.st_size, sizeof( short ) + 16 ); + + statsBuffer.Clear(); + statsBuffer.EnsureCapacity( nBytesToRead ); + fread( statsBuffer.Base(), nBytesToRead, 1, fp ); + statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, nBytesToRead ); + fclose( fp ); + + // Skip the version + statsBuffer.GetShort(); + statsBuffer.Get( pchUserID, 16 ); + pchUserID[ bufsize - 1 ] = 0; + + modifiedtime = sb.st_mtime; + + return statsBuffer.TellPut() == statsBuffer.TellGet(); + } + + return false; +} + +int Ep2_ParseCustomGameStatsData( ParseContext_t *ctx ) +{ + g_dictMapStats.Purge(); + + FILE *fp = fopen( ctx->file, "rb" ); + if ( fp ) + { + CUtlBuffer statsBuffer; + + struct _stat sb; + _stat( ctx->file, &sb ); + + // Msg( "Processing %s\n", ctx->file ); + + statsBuffer.Clear(); + statsBuffer.EnsureCapacity( sb.st_size ); + fread( statsBuffer.Base(), sb.st_size, 1, fp ); + statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size ); + fclose( fp ); + + char shortname[ 128 ]; + Q_FileBase( ctx->file, shortname, sizeof( shortname ) ); + + char szCurrentStatsFileUserID[17]; + int iCurrentStatsFileVersion; + + iCurrentStatsFileVersion = statsBuffer.GetShort(); + statsBuffer.Get( szCurrentStatsFileUserID, 16 ); + szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0; + + bool valid = true; + BasicGameStats_t stats; + + char tag[ 32 ] = { 0 }; + + unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt(); + if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED ) + { + //standard data was saved, rewind so the stats can read in time to completion + statsBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, -((int)sizeof( unsigned int )) ); + + valid = stats.ParseFromBuffer( statsBuffer, iCurrentStatsFileVersion ); + + if ( ctx->describeonly ) + { + DescribeData( stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion ); + } + } + + //check for custom data + bool bHasCustomData = (valid && (statsBuffer.TellPut() != statsBuffer.TellGet())); + + if( bHasCustomData ) + { + LoadCustomDataFromBuffer( statsBuffer, tag, sizeof( tag ) ); + + if( ctx->describeonly ) + { + DescribeEp2Stats(); + } + else + { + InsertCustomData( g_mapOrder, ctx->mysql, szCurrentStatsFileUserID, iCurrentStatsFileVersion, ctx->gamename ); + } + } + + // Do base data after custom since we need the custom to parse out the "tag" used for this user + if ( valid ) + { + if ( !ctx->describeonly ) + { + InsertData( g_mapOrder, ctx->mysql, stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion, ctx->gamename, tag ); + } + } + else + { + ++ctx->skipcount; + } + } + return CUSTOMDATA_SUCCESS; +} + diff --git a/devtools/processgamestats/portal_gamestats.db b/devtools/processgamestats/portal_gamestats.db new file mode 100644 index 0000000..65acddf --- /dev/null +++ b/devtools/processgamestats/portal_gamestats.db @@ -0,0 +1,47 @@ +# create database gamestats; +# use gamestats; + +create table portal +( + UserID CHAR(16), + PRIMARY KEY( UserID ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + hl2_chapter TINYINT, + SecondsToCompleteGame INT, # Non-zero if user has completed game + HighestMap CHAR(16), + DXLevel INT, + Deaths INT, +) TYPE=MyISAM; + +create table portal_maps +( + UserID CHAR(16), + MapName CHAR(16), + PRIMARY KEY( UserID,MapName ), + LastUpdate DATETIME, + KEY( LastUpdate ), + Version TINYINT, + Count INT, + Seconds INT, + HDR INT, + Captions INT, + Commentary INT, + Easy INT, + Medium INT, + Hard INT, + nonsteam TINYINT, + cybercafe TINYINT, + Deaths INT, +) TYPE=MyISAM;
\ No newline at end of file diff --git a/devtools/processgamestats/processgamestats.cpp b/devtools/processgamestats/processgamestats.cpp new file mode 100644 index 0000000..05e643f --- /dev/null +++ b/devtools/processgamestats/processgamestats.cpp @@ -0,0 +1,982 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// tagbuild.cpp : Defines the entry point for the console application. +// + +#include "stdafx.h" +#include <stdio.h> +#include <process.h> +#include <string.h> +#include <windows.h> +#include <sys/stat.h> +#include <time.h> + +#include "interface.h" +#include "imysqlwrapper.h" +#include "tier1/utlvector.h" +#include "tier1/utlbuffer.h" +#include "tier1/utlsymbol.h" +#include "tier1/utlstring.h" +#include "tier1/utldict.h" +#include "KeyValues.h" +#include "filesystem_helpers.h" +#include "tier2/tier2.h" +#include "filesystem.h" +#include "base_gamestats_parse.h" +#include "cbase.h" +#include "gamestats.h" +#include "tier0/icommandline.h" + +// roll-our-own symbol table class. Note we don't use CUtlSymbolTable because that and related classes have short int deeply baked in as index type, so can +// only hold 64K entries. We sometimes need to process more than 64K files at a time. +struct AnalysisData +{ + AnalysisData() + { + symbols.SetLessFunc( CaselessStringLessThanIgnoreSlashes ); + } + + ~AnalysisData() + { + int i = symbols.FirstInorder(); + while ( i != symbols.InvalidIndex() ) + { + const char *symbol = symbols[i]; + if ( symbol ) + { + delete symbol; + } + i = symbols.NextInorder( i ); + } + } + + CUtlRBTree<const char*,int> symbols; +}; + +static AnalysisData g_Analysis; + +static bool describeonly = false; + +typedef int (*DataParseFunc)( ParseContext_t * ); +typedef void (*PostImportFunc) ( IMySQL *sql ); +typedef bool (*ParseCurrentUserIDFunc)( char const *pchDataFile, char *pchUserID, size_t bufsize, time_t &modifiedtime ); + +extern int CS_ParseCustomGameStatsData( ParseContext_t *ctx ); +extern int Ep2_ParseCustomGameStatsData( ParseContext_t *ctx ); +extern int TF_ParseCustomGameStatsData( ParseContext_t *ctx ); +extern void TF_PostImport( IMySQL *sql ); + +int Default_ParseCustomGameStatsData( ParseContext_t *ctx ); + +extern bool Ep2_ParseCurrentUserID( char const *pchDataFile, char *pchUserID, size_t bufsize, time_t &modifiedtime ); + +struct DataParser_t +{ + char const *pchGameName; + DataParseFunc pfnParseFunc; + PostImportFunc pfnPostImport; + ParseCurrentUserIDFunc pfnParseUserID; +}; + +static DataParser_t g_ParseFuncs[] = +{ + { "cstrike", CS_ParseCustomGameStatsData, NULL }, + { "tf", TF_ParseCustomGameStatsData, TF_PostImport }, +// { "dods", Default_ParseCustomGameStatsData, NULL }, +// { "portal", Default_ParseCustomGameStatsData, NULL }, + { "ep1", Default_ParseCustomGameStatsData, NULL }, // Just a STUB + { "ep2", Ep2_ParseCustomGameStatsData, NULL, Ep2_ParseCurrentUserID } +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void printusage( void ) +{ + printf( "processgamestats:\n" ); + printf( "processgamestats game dbhost user password dbname rootdir\n" ); + printf( "processgamestats game datafile [describe only]\n\n" ); + printf( "valid gamenames:\n" ); + + for ( int i = 0 ; i < ARRAYSIZE( g_ParseFuncs ); ++i ) + { + printf( " %s\n", g_ParseFuncs[ i ].pchGameName ); + } + + // Exit app + exit( 1 ); +} + +void BuildFileList_R( CUtlVector< int >& files, char const *dir, char const *extension ) +{ + WIN32_FIND_DATA wfd; + + char directory[ 256 ]; + char filename[ 256 ]; + HANDLE ff; + + sprintf( directory, "%s\\*.*", dir ); + + if ( ( ff = FindFirstFile( directory, &wfd ) ) == INVALID_HANDLE_VALUE ) + return; + + int extlen = strlen( extension ); + + do + { + if ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) + { + + if ( wfd.cFileName[ 0 ] == '.' ) + continue; + + // Recurse down directory + sprintf( filename, "%s\\%s", dir, wfd.cFileName ); + BuildFileList_R( files, filename, extension ); + } + else + { + int len = strlen( wfd.cFileName ); + if ( len > extlen ) + { + if ( !stricmp( &wfd.cFileName[ len - extlen ], extension ) ) + { + char filename[ MAX_PATH ]; + Q_snprintf( filename, sizeof( filename ), "%s\\%s", dir, wfd.cFileName ); + _strlwr( filename ); + + Q_FixSlashes( filename ); + + char *symbol = strdup( filename ); + int sym = g_Analysis.symbols.Insert( symbol ); + + files.AddToTail( sym ); + } + } + } + } while ( FindNextFile( ff, &wfd ) ); +} + +void BuildFileList( CUtlVector< int >& files, char const *rootdir, char const *extension ) +{ + files.RemoveAll(); + BuildFileList_R( files, rootdir, extension ); +} + +void DescribeData( BasicGameStats_t &stats, const char *szStatsFileUserID, int iStatsFileVersion ) +{ + double averageSession = 0.0f; + if ( stats.m_Summary.m_nCount > 0 ) + { + averageSession = (double)stats.m_Summary.m_nSeconds / (double)stats.m_Summary.m_nCount; + } + + Msg( "---------------------------------------------------------------------------\n" ); + Msg( "%16.16s : %s\n", "User", szStatsFileUserID ); + Msg( " %16.16s: %8d\n", "Blob version", iStatsFileVersion ); + Msg( " %16.16s: %8d sessions\n", "Played", stats.m_Summary.m_nCount ); + Msg( " %16.16s: %8d seconds\n", "Total Time", stats.m_Summary.m_nSeconds ); + Msg( " %16.16s: %8.2f seconds\n", "Avg Session", averageSession ); + + Msg( " %16.16s: %8d\n", "Commentary", stats.m_Summary.m_nCommentary ); + Msg( " %16.16s: %8d\n", "HDR", stats.m_Summary.m_nHDR ); + Msg( " %16.16s: %8d\n", "Captions", stats.m_Summary.m_nCaptions ); + Msg( " %16.16s: %8d\n", "Easy", stats.m_Summary.m_nSkill[ 0 ] ); + Msg( " %16.16s: %8d\n", "Medium", stats.m_Summary.m_nSkill[ 1 ] ); + Msg( " %16.16s: %8d\n", "Hard", stats.m_Summary.m_nSkill[ 2 ] ); + Msg( " %16.16s: %8d seconds\n", "Completion time ", stats.m_nSecondsToCompleteGame ); + Msg( " %16.16s: %8d\n", "Number of deaths", stats.m_Summary.m_nDeaths ); + + Msg( " -- Maps played --\n" ); + + for ( int i = stats.m_MapTotals.First(); i != stats.m_MapTotals.InvalidIndex(); i = stats.m_MapTotals.Next( i ) ) + { + char const *mapname = stats.m_MapTotals.GetElementName( i ); + BasicGameStatsRecord_t &rec = stats.m_MapTotals[ i ]; + + Msg( " %16.16s: %5d seconds in %3d sessions (%4d deaths)\n", mapname, rec.m_nSeconds, rec.m_nCount, rec.m_nDeaths ); + } +} + +#include <string> +//------------------------------------------------- +void v_escape_string (std::string& s) +{ + if ( !s.size() ) + return; + for ( unsigned int i = 0;i<s.size();i++ ) + { + switch (s[i]) + { + case '\0': /* Must be escaped for "mysql" */ + s[i] = '\\'; + s.insert(i+1,"0",1); i++;//lint !e534 + break; + case '\n': /* Must be escaped for logs */ + s[i] = '\\'; + s.insert(i+1,"n",1); i++;//lint !e534 + break; + case '\r': + s[i] = '\\'; + s.insert(i+1,"r",1); i++;//lint !e534 + break; + case '\\': + s[i] = '\\'; + s.insert(i+1,"\\",1); i++;//lint !e534 + break; + case '\"': + s[i] = '\\'; + s.insert(i+1,"\"",1); i++;//lint !e534 + break; + case '\'': /* Better safe than sorry */ + s[i] = '\\'; + s.insert(i+1,"\'",1); i++;//lint !e534 + break; + case '\032': /* This gives problems on Win32 */ + s[i] = '\\'; + s.insert(i+1,"Z",1); i++;//lint !e534 + break; + default: + break; + } + } +} + +void InsertData( CUtlDict< int, unsigned short >& mapOrder, IMySQL *sql, BasicGameStats_t &gs, const char *szStatsFileUserID, int iStatsFileVersion, const char *gamename, char const *tag = NULL ) +{ + if ( !sql ) + return; + + char q[ 512 ]; + + std::string userid; + userid = szStatsFileUserID; + v_escape_string( userid ); + + int farthestPlayed = -1; + std::string highestmap; + + int namelen = 20; + if ( !Q_stricmp( gamename, "ep1" ) ) + { + namelen = 16; + } + + char finalname[ 64 ]; + + std::string finaltag; + finaltag = tag ? tag : ""; + v_escape_string( finaltag ); + + // Deal with the maps + for ( int i = gs.m_MapTotals.First(); i != gs.m_MapTotals.InvalidIndex(); i = gs.m_MapTotals.Next( i ) ) + { + char const *pszMapName = gs.m_MapTotals.GetElementName( i ); + std::string mapname; + mapname = pszMapName; + v_escape_string( mapname ); + + Q_strncpy( finalname, mapname.c_str(), namelen ); + + int slot = mapOrder.Find( pszMapName ); + if ( slot != mapOrder.InvalidIndex() ) + { + int order = mapOrder[ slot ]; + if ( order > farthestPlayed ) + { + farthestPlayed = order; + } + } + else + { + if ( Q_stricmp( pszMapName, "devtest" ) ) + continue; + } + + + + BasicGameStatsRecord_t& rec = gs.m_MapTotals[ i ]; + + if ( tag ) + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s_maps (UserID,LastUpdate,Version,MapName,Tag,Count,Seconds,HDR,Captions,Commentary,Easy,Medium,Hard,nonsteam,cybercafe,Deaths) values (\"%s\",Now(),%d,\"%s\",\"%s\",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);", + gamename, + userid.c_str(), + iStatsFileVersion, + finalname, + finaltag.c_str(), + rec.m_nCount, + rec.m_nSeconds, + rec.m_nHDR, + rec.m_nCaptions, + rec.m_nCommentary, + rec.m_nSkill[ 0 ], + rec.m_nSkill[ 1 ], + rec.m_nSkill[ 2 ], + rec.m_bSteam ? 0 : 1, + rec.m_bCyberCafe ? 1 : 0, + rec.m_nDeaths ); + } + else + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s_maps (UserID,LastUpdate,Version,MapName,Count,Seconds,HDR,Captions,Commentary,Easy,Medium,Hard,nonsteam,cybercafe,Deaths) values (\"%s\",Now(),%d,\"%s\",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);", + gamename, + userid.c_str(), + iStatsFileVersion, + finalname, + rec.m_nCount, + rec.m_nSeconds, + rec.m_nHDR, + rec.m_nCaptions, + rec.m_nCommentary, + rec.m_nSkill[ 0 ], + rec.m_nSkill[ 1 ], + rec.m_nSkill[ 2 ], + rec.m_bSteam ? 0 : 1, + rec.m_bCyberCafe ? 1 : 0, + rec.m_nDeaths ); + } + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + + if ( farthestPlayed != -1 ) + { + highestmap = mapOrder.GetElementName( farthestPlayed ); + } + v_escape_string( highestmap ); + Q_strncpy( finalname, highestmap.c_str(), namelen ); + + if ( tag ) + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s (UserID,LastUpdate,Version,Tag,Count,Seconds,HDR,Captions,Commentary,Easy,Medium,Hard,SecondsToCompleteGame,HighestMap,nonsteam,cybercafe,hl2_chapter,dxlevel,Deaths) values (\"%s\",Now(),%d,\"%s\",%d,%d,%d,%d,%d,%d,%d,%d,%d,\"%s\",%d,%d,%d,%d,%d);", + + gamename, + userid.c_str(), + iStatsFileVersion, + finaltag.c_str(), + gs.m_Summary.m_nCount, + gs.m_Summary.m_nSeconds, + gs.m_Summary.m_nHDR, + gs.m_Summary.m_nCaptions, + gs.m_Summary.m_nCommentary, + gs.m_Summary.m_nSkill[ 0 ], + gs.m_Summary.m_nSkill[ 1 ], + gs.m_Summary.m_nSkill[ 2 ], + gs.m_nSecondsToCompleteGame, + finalname, + gs.m_bSteam ? 0 : 1, + gs.m_bCyberCafe ? 1 : 0, + gs.m_nHL2ChaptureUnlocked, + gs.m_nDXLevel, + gs.m_Summary.m_nDeaths ); + } + else + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s (UserID,LastUpdate,Version,Count,Seconds,HDR,Captions,Commentary,Easy,Medium,Hard,SecondsToCompleteGame,HighestMap,nonsteam,cybercafe,hl2_chapter,dxlevel,Deaths) values (\"%s\",Now(),%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,\"%s\",%d,%d,%d,%d,%d);", + + gamename, + userid.c_str(), + iStatsFileVersion, + gs.m_Summary.m_nCount, + gs.m_Summary.m_nSeconds, + gs.m_Summary.m_nHDR, + gs.m_Summary.m_nCaptions, + gs.m_Summary.m_nCommentary, + gs.m_Summary.m_nSkill[ 0 ], + gs.m_Summary.m_nSkill[ 1 ], + gs.m_Summary.m_nSkill[ 2 ], + gs.m_nSecondsToCompleteGame, + finalname, + gs.m_bSteam ? 0 : 1, + gs.m_bCyberCafe ? 1 : 0, + gs.m_nHL2ChaptureUnlocked, + gs.m_nDXLevel, + gs.m_Summary.m_nDeaths ); + } + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } +} +CUtlDict< int, unsigned short > g_mapOrder; + +void BuildMapList( void ) +{ + void *buffer = NULL; + char *pFileList; + FILE * pFile; + pFile = fopen ("maplist.txt", "r"); + int i = 0; + + if ( pFile ) + { + long lSize; + // obtain file size. + fseek (pFile , 0 , SEEK_END); + lSize = ftell (pFile); + rewind (pFile); + + // allocate memory to contain the whole file. + buffer = (char*) malloc (lSize); + if ( buffer != NULL ) + { + // copy the file into the buffer. + fread (buffer,1,lSize,pFile); + pFileList = (char*)buffer; + char szToken[1024]; + + while ( 1 ) + { + pFileList = ParseFile( pFileList, szToken, false ); + + if ( pFileList == NULL ) + break; + + g_mapOrder.Insert( szToken, i ); + i++; + } + } + + fclose( pFile ); + free( buffer ); + } + else + { + Msg( "Couldn't load maplist.txt for mod!!!\n" ); + } +} + +int Default_ParseCustomGameStatsData( ParseContext_t *ctx ) +{ + FILE *fp = fopen( ctx->file, "rb" ); + if ( fp ) + { + CUtlBuffer statsBuffer; + + struct _stat sb; + _stat( ctx->file, &sb ); + + statsBuffer.Clear(); + statsBuffer.EnsureCapacity( sb.st_size ); + fread( statsBuffer.Base(), sb.st_size, 1, fp ); + statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size ); + fclose( fp ); + + char shortname[ 128 ]; + Q_FileBase( ctx->file, shortname, sizeof( shortname ) ); + + char szCurrentStatsFileUserID[17]; + int iCurrentStatsFileVersion; + + iCurrentStatsFileVersion = statsBuffer.GetShort(); + statsBuffer.Get( szCurrentStatsFileUserID, 16 ); + szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0; + + bool valid = true; + + unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt(); + if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED ) + { + //standard data was saved, rewind so the stats can read in time to completion + statsBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, -((int)sizeof( unsigned int )) ); + + BasicGameStats_t stats; + valid = stats.ParseFromBuffer( statsBuffer, iCurrentStatsFileVersion ); + + if ( describeonly ) + { + DescribeData( stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion ); + } + else + { + if ( valid ) + { + InsertData( g_mapOrder, ctx->mysql, stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion, ctx->gamename ); + } + else + { + ++ctx->skipcount; + } + } + } + + //check for custom data + bool bHasCustomData = (valid && (statsBuffer.TellPut() != statsBuffer.TellGet())); + + if( bHasCustomData ) + { + if( describeonly ) + { + //separate out the custom data and store it off for processing by other applications, + //since they only wanted to 'describe' the data, just use a local temp and overwrite it each time + + const char *szCustomDataOutputFileName = "customdata_temp.dat"; + + Msg( "\n\nFound custom data, dumping to %s\n", szCustomDataOutputFileName ); + + FILE *pCustomDataOutput = fopen( szCustomDataOutputFileName, "wb+" ); + if( pCustomDataOutput ) + { + int iGetPosition = statsBuffer.TellGet(); + fwrite( (((unsigned char *)statsBuffer.Base()) + iGetPosition), statsBuffer.TellPut() - iGetPosition, 1, pCustomDataOutput ); + fclose( pCustomDataOutput ); + } + } + else + { + //separate out the custom data and store it off for processing by other applications, + //assume we will have multiple input stats files from the same user, so store custom data under their userid name and overwrite old data to avoid bloat + if( ctx->bCustomDirectoryNotMade ) + { + CreateDirectory( "customdatadumps", NULL ); + ctx->bCustomDirectoryNotMade = false; + } + + char szCustomDataOutputFileName[256]; + Q_snprintf( szCustomDataOutputFileName, sizeof( szCustomDataOutputFileName ), "customdatadumps/%s.dat", szCurrentStatsFileUserID ); + + FILE *pCustomDataOutput = fopen( szCustomDataOutputFileName, "wb+" ); + if( pCustomDataOutput ) + { + int iGetPosition = statsBuffer.TellGet(); + fwrite( (((unsigned char *)statsBuffer.Base()) + iGetPosition), statsBuffer.TellPut() - iGetPosition, 1, pCustomDataOutput ); + fclose( pCustomDataOutput ); + } + } + } + } + return CUSTOMDATA_SUCCESS; +} + +int main(int argc, char* argv[]) +{ + CommandLine()->CreateCmdLine( argc, argv ); + + ParseContext_t ctx; + + if ( argc < 7 && argc != 3 ) + { + printusage(); + } + + describeonly = argc == 3; + + int gameArg = 1; + int hostArg = 2; + int usernameArg = 3; + int pwArg = 4; + int dbArg = 5; + int dirArg = 6; + if ( describeonly ) + { + dirArg = 2; + } + + InitDefaultFileSystem(); + + BuildMapList(); + const char *gamename = argv[ gameArg ]; + DataParseFunc parseFunc = NULL; + PostImportFunc postImportFunc = NULL; + ParseCurrentUserIDFunc parseUserIDFunc = NULL; + for ( int i = 0 ; i < ARRAYSIZE( g_ParseFuncs ); ++i ) + { + if ( !Q_stricmp( g_ParseFuncs[ i ].pchGameName, gamename ) ) + { + parseFunc = g_ParseFuncs[ i ].pfnParseFunc; + postImportFunc = g_ParseFuncs[ i ].pfnPostImport; + parseUserIDFunc = g_ParseFuncs[ i ].pfnParseUserID; + break; + } + } + + if ( !parseFunc ) + { + printf( "Invalid game name '%s'\n", gamename ); + printusage(); + } + + bool batchMode = true; + + CUtlVector< int > files; + if ( describeonly || Q_stristr( argv[ dirArg ], ".dat" ) ) + { + char filename[ MAX_PATH ]; + Q_snprintf( filename, sizeof( filename ), "%s", argv[ dirArg ] ); + _strlwr( filename ); + Q_FixSlashes( filename ); + char *symbol = strdup( filename ); + int sym = g_Analysis.symbols.Insert( symbol ); + files.AddToTail( sym ); + + batchMode = false; + } + else + { + Msg( "Building file list\n" ); + BuildFileList( files, argv[ dirArg ], "dat" ); + } + + if ( !files.Count() ) + { + printf( "No files to operate upon\n" ); + exit( -1 ); + } + + int c = files.Count(); + + // Cull list of files by looking for most recent version of user's stats and only keeping around those files + if ( parseUserIDFunc ) + { + struct CUserIDFileMapping + { + CUserIDFileMapping() : + filename( UTL_INVAL_SYMBOL ), filemodifiedtime( 0 ), modcount( 1 ) + { + userid[ 0 ] = 0; + } + + char userid[ 17 ]; + CUtlSymbol filename; + time_t filemodifiedtime; + int modcount; + + static bool Less( const CUserIDFileMapping &lhs, const CUserIDFileMapping &rhs ) + { + return Q_stricmp( lhs.userid, rhs.userid ) < 0; + } + }; + + + CUtlRBTree< CUserIDFileMapping, int > userIDToFileMap( 0, 0, CUserIDFileMapping::Less ); + + int nDiscards = 0; + int nSkips =0; + int nMaxMod = 1; + for ( int i = 0; i < c; ++i ) + { + char const *fn = g_Analysis.symbols.Element( files[ i ] ); + + CUserIDFileMapping search; + search.filename = files[ i ]; + if ( (*parseUserIDFunc)( fn, search.userid, sizeof( search.userid ), search.filemodifiedtime ) ) + { + // Find map index + int idx = userIDToFileMap.Find( search ); + if ( idx == userIDToFileMap.InvalidIndex() ) + { + userIDToFileMap.Insert( search ); + } + else + { + CUserIDFileMapping &update = userIDToFileMap[ idx ]; + if ( search.filemodifiedtime > update.filemodifiedtime ) + { + update.filename = files[ i ]; + update.filemodifiedtime = search.filemodifiedtime; + update.modcount++; + if ( update.modcount > nMaxMod ) + { + nMaxMod = update.modcount; + } + } + ++nDiscards; + } + } + else + { + ++nSkips; + } + + if ( i > 0 && !( i % 100 ) ) + { + printf( "Parsing user ID's: [%-6.6d/%-6.6d] %.2f %% complete\n", i, c, 100.0f * (float)i/(float)c ); + } + } + + Msg( "discarded %d of %d, remainder %d [%d skipped] max mod %d\n", nDiscards, c, userIDToFileMap.Count(), nSkips, nMaxMod ); + + // Now re-write files and count with pared down listing + files.Purge(); + for( int i = userIDToFileMap.FirstInorder(); i != userIDToFileMap.InvalidIndex(); i = userIDToFileMap.NextInorder( i ) ) + { + files.AddToTail( userIDToFileMap[ i ].filename ); + } + c = files.Count(); + } + + bool bTrySql = !describeonly; + + bool bSqlOkay = false; + + CSysModule *sql = NULL; + CreateInterfaceFn factory = NULL; + IMySQL *mysql = NULL; + + if ( bTrySql ) + { + // Now connect to steamweb and update the engineaccess table + sql = Sys_LoadModule( "mysql_wrapper" ); + if ( sql ) + { + factory = Sys_GetFactory( sql ); + if ( factory ) + { + mysql = ( IMySQL * )factory( MYSQL_WRAPPER_VERSION_NAME, NULL ); + if ( mysql ) + { + if ( mysql->InitMySQL( argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ], argv[ pwArg ] ) ) + { + bSqlOkay = true; + if ( batchMode ) + { + Msg( "Successfully connected to database %s on host %s, user %s\n", + argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ] ); + } + } + else + { + Msg( "mysql->InitMySQL( %s, %s, %s, [password]) failed\n", + argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ] ); + } + } + else + { + Msg( "Unable to get MYSQL_WRAPPER_VERSION_NAME(%s) from mysql_wrapper\n", MYSQL_WRAPPER_VERSION_NAME ); + } + } + else + { + Msg( "Sys_GetFactory on mysql_wrapper failed\n" ); + } + } + else + { + Msg( "Sys_LoadModule( mysql_wrapper ) failed\n" ); + } + } + + ctx.gamename = gamename; + ctx.describeonly = describeonly; + ctx.mysql = mysql; + ctx.skipcount = 0; + ctx.bCustomDirectoryNotMade = true; + + if ( bSqlOkay || describeonly ) + { + + for ( int i = 0; i < c; ++i ) + { + char const *fn = g_Analysis.symbols.Element( files[ i ] ); + + ctx.file = fn; + + int iCustomData = (*parseFunc)( &ctx ); + if ( iCustomData == CUSTOMDATA_SUCCESS ) + { + if ( i > 0 && !( i % 100 ) ) + { + printf( "Processing: [%-6.6d/%-6.6d] %.2f %% complete\n", i, c, 100.0f * (float)i/(float)c ); + } + } + } + + if ( ctx.skipcount > 0 ) + { + printf( "Skipped %d samples which appear to be malformed or contain bogus data\n", ctx.skipcount ); + } + + // if this game has a post-import function to call after all the files have been imported, call it now + if ( bSqlOkay && postImportFunc ) + { + postImportFunc( mysql ); + } + } + + if ( bSqlOkay ) + { + if ( mysql ) + { + mysql->Release(); + mysql = NULL; + } + + if ( sql ) + { + Sys_UnloadModule( sql ); + sql = NULL; + } + } + + return 0; +} + + +static void OverWriteCharsWeHate( char *pStr ) +{ + while( *pStr ) + { + switch( *pStr ) + { + case '\n': + case '\r': + case '\\': + case '\"': + case '\'': + case '\032': + case ';': + *pStr = ' '; + } + pStr++; + } +} + +void InsertKeyDataIntoTable( IMySQL *pSQL, time_t fileTime, char const *pTableName, char const *pPerfData, char const *pKeyWhiteList[], int nNumFields ) +{ + char szDate[128]="Now()"; + if ( fileTime > 0 ) + { + tm * pTm = localtime( &fileTime ); + Q_snprintf( szDate, ARRAYSIZE( szDate ), "'%04d-%02d-%02d %02d:%02d:%02d'", + pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec ); + } + + // we don't need to worry about semicolons embedded in string fields because we supressed them + // on the client. if some malicious person inserts them, the mandled field names will fail the + // whitelist check, causing the record to be ignored. + CUtlVector<char *> tokens; + // split into tokens at non-quoated spaces or ;'s + for(;;) + { + char const *pStr = pPerfData; + if ( pStr[0] == 0 ) + break; + while( pStr[0] && ( pStr[0] != ' ' ) && ( pStr[0] != ';' ) ) + { + if ( pStr[0]=='"') + { + // skip to end quote + char const *pEq = strchr( pStr + 1, '\"' ); + if ( ! pEq ) + { + printf(" close quote with no open quote\n" ); + return; + } + pStr = pEq; + } + pStr++; + } + // got a field + int nlen = pStr - pPerfData; + if ( nlen > 2 ) + { + char *pToken = new char[ nlen + 1 ]; + memcpy( pToken, pPerfData, nlen ); + pToken[nlen] = 0; + tokens.AddToTail( pToken ); + } + if ( pStr[0] ) + pStr++; + pPerfData = pStr; + } + + bool bBadData = false; + char fieldNameBuffer[1024]; + char fieldValueBuffer[2048]; + strcpy( fieldNameBuffer, "(CreationTimeStamp, " ); + Q_snprintf( fieldValueBuffer, ARRAYSIZE( fieldValueBuffer), "( %s,", szDate ); + for( int i = 0; i < tokens.Count(); i++ ) + { + char *pKVData = tokens[i]; + char *pEqualsSign = strchr( pKVData, '=' ); + if (! pEqualsSign ) + { + bBadData = true; + break; + } + *pEqualsSign = 0; // *semicolon->null + // check that the field is in the white list + bool bFoundIt = false; + for( int nCheck = 0; nCheck < nNumFields; nCheck++ ) + if ( strcmp( pKVData, pKeyWhiteList[nCheck] ) == 0 ) + { + bFoundIt = true; + break; + } + V_strncat( fieldNameBuffer, pKVData, sizeof( fieldNameBuffer ) ); + if ( i != tokens.Count() -1 ) + V_strncat( fieldNameBuffer, ",", sizeof( fieldNameBuffer ) ); + else + V_strncat( fieldNameBuffer, ")", sizeof( fieldNameBuffer ) ); + char *pValue = pEqualsSign + 1; + OverWriteCharsWeHate( pValue ); + if ( ( strlen( pValue ) < 1 ) || (! bFoundIt ) ) + { + bBadData = true; + break; + } + // kill lead + trail space + if ( pValue[0] == ' ' ) + pValue++; + if ( pValue[strlen(pValue) - 1 ] == ' ' ) + pValue[strlen( pValue ) - 1 ] =0; + V_strncat( fieldValueBuffer, "'", sizeof( fieldValueBuffer ) ); + V_strncat( fieldValueBuffer, pValue, sizeof( fieldValueBuffer ) ); + if ( i != tokens.Count() -1 ) + V_strncat( fieldValueBuffer, "',", sizeof( fieldValueBuffer ) ); + else + V_strncat( fieldValueBuffer, "')", sizeof( fieldValueBuffer ) ); + } + if (! bBadData ) + { + char sqlCommandBuffer[1024 + sizeof( fieldNameBuffer ) + sizeof( fieldValueBuffer ) ]; + sprintf( sqlCommandBuffer, "insert into %s %s values %s;", pTableName, fieldNameBuffer, fieldValueBuffer ); +// printf("cmd %s\n", sqlCommandBuffer); + int retcode = pSQL->Execute( sqlCommandBuffer ); + if ( retcode != 0 ) + { + printf( "command %s failed\n", sqlCommandBuffer ); + } + } + + tokens.PurgeAndDeleteElements(); +} + +char const *s_PerfKeyList[] = { + "AvgFps", + "MinFps", + "MaxFps", + "CPUID", + "CPUGhz", + "NumCores", + "GPUDrv", + "GPUVendor", + "GPUDeviceID", + "GPUDriverVersion", + "DxLvl", + "Width", + "Height", + "MapName", + "TotalLevelTime", + "NumLevels" +}; + +void ProcessPerfData( IMySQL *pSQL, time_t fileTime, char const *pTableName, char const *pPerfData ) +{ + InsertKeyDataIntoTable( pSQL, fileTime, pTableName, pPerfData, s_PerfKeyList, ARRAYSIZE( s_PerfKeyList) ); +} + diff --git a/devtools/processgamestats/processgamestats.vpc b/devtools/processgamestats/processgamestats.vpc new file mode 100644 index 0000000..8b8d184 --- /dev/null +++ b/devtools/processgamestats/processgamestats.vpc @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------------- +// PROCESSGAMESTATS.VPC +// +// Project Script +//----------------------------------------------------------------------------- + +$Macro SRCDIR "..\.." +$Macro OUTBINDIR "$SRCDIR\..\game\bin" + +$Include "$SRCDIR\vpc_scripts\source_exe_con_base.vpc" + +$Configuration +{ + $Compiler + { + $AdditionalIncludeDirectories "$BASE;$SRCDIR\utils\vmpi;$SRCDIR\game\shared;$SRCDIR\game\server;$SRCDIR\game\shared\tf;$SRCDIR\game\shared\econ;$SRCDIR\gcsdk\steamextra" + $EnableC++Exceptions "Yes (/EHsc)" + $PreprocessorDefinitions "$BASE;GAME_DLL;TF_DLL;NO_STEAM" + } +} + +$Project "Processgamestats" +{ + $Folder "Source Files" + { + $File "$SRCDIR\public\filesystem_helpers.cpp" + $File "$SRCDIR\public\filesystem_helpers.h" + $File "$SRCDIR\game\server\GameStats_BasicStatsFunctions.cpp" + $File "processgamestats.cpp" + + $Folder "Custom Stats" + { + $File "base_gamestats_parse.h" + $File "cs_gamestats.h" + $File "cstrike_gamestats_parse.cpp" + $File "$SRCDIR\game\server\episodic\ep2_gamestats.h" + $File "ep2_gamestats_parse.cpp" + $File "$SRCDIR\game\server\tf\tf_gamestats.h" + $File "tf_gamestats_parse.cpp" + $File "$SRCDIR\game\shared\tf\tf_gamestats_shared.cpp" + $File "$SRCDIR\game\shared\tf\tf_gamestats_shared.h" + } + } + + $Folder "Header Files" + { + $File "$SRCDIR\public\tier0\basetypes.h" + $File "$SRCDIR\public\tier0\commonmacros.h" + $File "$SRCDIR\public\tier0\dbg.h" + $File "$SRCDIR\public\tier0\fasttimer.h" + $File "$SRCDIR\game\shared\gamestats.h" + $File "$SRCDIR\public\tier0\icommandline.h" + $File "$SRCDIR\utils\vmpi\imysqlwrapper.h" + $File "$SRCDIR\public\tier0\memdbgoff.h" + $File "$SRCDIR\public\tier0\memdbgon.h" + $File "$SRCDIR\public\tier0\platform.h" + $File "$SRCDIR\public\tier0\protected_things.h" + $File "$SRCDIR\public\string_t.h" + $File "$SRCDIR\public\tier1\strtools.h" + $File "$SRCDIR\public\tier1\utlmemory.h" + $File "$SRCDIR\public\tier1\utlvector.h" + $File "$SRCDIR\public\vstdlib\vstdlib.h" + } + + $Folder "Database Setup" + { + $File "cstrike_gamestats.db" + $File "portal_gamestats.db" + $File "ep1_gamestats.db" + $File "ep2_gamestats.db" + $File "tf_gamestats.db" + } + + $Folder "Link Libraries" + { + $Lib tier2 + } +} diff --git a/devtools/processgamestats/tf_gamestats.db b/devtools/processgamestats/tf_gamestats.db new file mode 100644 index 0000000..7f62b7c --- /dev/null +++ b/devtools/processgamestats/tf_gamestats.db @@ -0,0 +1,227 @@ +create database if not exists gamestats_tf; +use gamestats_tf; + +create table if not exists tf_mapdata +( + MapName VARCHAR(64) +); +alter table tf_mapdata add INDEX(MapName); +alter table tf_mapdata add column ServerID VARCHAR(16); +alter table tf_mapdata add column TimeSubmitted DATETIME; +alter table tf_mapdata add INDEX(TimeSubmitted); +alter table tf_mapdata add column RoundsPlayed BIGINT; +alter table tf_mapdata add column TotalTime BIGINT; +alter table tf_mapdata add column BlueWins BIGINT; +alter table tf_mapdata add column RedWins BIGINT; +alter table tf_mapdata add column Stalemates BIGINT; + + +create table if not exists tf_mapdata_rollup +( + MapName VARCHAR(64) +); +alter table tf_mapdata_rollup add INDEX(MapName); +alter table tf_mapdata_rollup add column TimeSubmitted DATETIME; +alter table tf_mapdata_rollup add INDEX(TimeSubmitted); +alter table tf_mapdata_rollup add column RoundsPlayed BIGINT; +alter table tf_mapdata_rollup add column TotalTime BIGINT; +alter table tf_mapdata_rollup add column BlueWins BIGINT; +alter table tf_mapdata_rollup add column RedWins BIGINT; +alter table tf_mapdata_rollup add column Stalemates BIGINT; + + +create table if not exists tf_classdata +( + MapName VARCHAR(64) +); +alter table tf_classdata add INDEX(MapName); +alter table tf_classdata add column ServerID VARCHAR(16); +alter table tf_classdata add column TimeSubmitted DATETIME; +alter table tf_classdata add INDEX(TimeSubmitted); +alter table tf_classdata add column Class TINYINT; +alter table tf_classdata add column Spawns BIGINT; +alter table tf_classdata add column TotalTime BIGINT; +alter table tf_classdata add column Score BIGINT; +alter table tf_classdata add column Kills BIGINT; +alter table tf_classdata add column Deaths BIGINT; +alter table tf_classdata add column Assists BIGINT; +alter table tf_classdata add column Captures BIGINT; + +create table if not exists tf_classdata_rollup +( + MapName VARCHAR(64) +); +alter table tf_classdata_rollup add INDEX(MapName); +alter table tf_classdata_rollup add column TimeSubmitted DATETIME; +alter table tf_classdata_rollup add INDEX(TimeSubmitted); +alter table tf_classdata_rollup add column Class TINYINT; +alter table tf_classdata_rollup add column Spawns BIGINT; +alter table tf_classdata_rollup add column TotalTime BIGINT; +alter table tf_classdata_rollup add column Score BIGINT; +alter table tf_classdata_rollup add column Kills BIGINT; +alter table tf_classdata_rollup add column Deaths BIGINT; +alter table tf_classdata_rollup add column Assists BIGINT; +alter table tf_classdata_rollup add column Captures BIGINT; + +create table if not exists tf_weapondata +( + WeaponID TINYINT +); +alter table tf_weapondata add INDEX(WeaponID); +alter table tf_weapondata add column ServerID VARCHAR(16); +alter table tf_weapondata add column MapName VARCHAR(64); +alter table tf_weapondata add INDEX(MapName); +alter table tf_weapondata add column TimeSubmitted DATETIME; +alter table tf_weapondata add INDEX(TimeSubmitted); +alter table tf_weapondata add column ShotsFired BIGINT; +alter table tf_weapondata add column ShotsFiredCrit BIGINT; +alter table tf_weapondata add column ShotsHit BIGINT; +alter table tf_weapondata add column DamageTotal BIGINT; +alter table tf_weapondata add column HitsWithKnownDistance BIGINT; +alter table tf_weapondata add column DistanceTotal BIGINT; + +create table if not exists tf_weapondata_rollup +( + WeaponID TINYINT +); +alter table tf_weapondata_rollup add INDEX(WeaponID); +alter table tf_weapondata_rollup add column MapName VARCHAR(64); +alter table tf_weapondata_rollup add INDEX(MapName); +alter table tf_weapondata_rollup add column TimeSubmitted DATETIME; +alter table tf_weapondata_rollup add INDEX(TimeSubmitted); +alter table tf_weapondata_rollup add column ShotsFired BIGINT; +alter table tf_weapondata_rollup add column ShotsFiredCrit BIGINT; +alter table tf_weapondata_rollup add column ShotsHit BIGINT; +alter table tf_weapondata_rollup add column DamageTotal BIGINT; +alter table tf_weapondata_rollup add column HitsWithKnownDistance BIGINT; +alter table tf_weapondata_rollup add column DistanceTotal BIGINT; + +create table if not exists tf_deaths +( + UserID CHAR(16), + Tag CHAR(8), + KEY( Tag ), + MapName CHAR(20), + MapVersion INT, + KEY( MapVersion ), + LastUpdate DATETIME, + KEY( LastUpdate ), + DeathIndex INT, + X SMALLINT, + Y SMALLINT, + Z SMALLINT, + PRIMARY KEY ( UserID, Tag, MapName, X, Y, Z ) +) TYPE=MyISAM; + + +create table if not exists tf_perfdata +( + CreationTimeStamp DATETIME, + AvgFps FLOAT, + MinFps FLOAT, + MaxFps FLOAT, + CPUID VARCHAR(64), + CPUGhz FLOAT, + NumCores INT, + GPUDrv VARCHAR(64), + GPUVendor INT, + GPUDeviceID INT, + GPUDriverVersion VARCHAR(25), + DxLvl INT, + Width INT, + Height INT, + MapName VARCHAR(64), + TotalLevelTime INT, + NumLevels SMALLINT, + INDEX(CreationTimeStamp), + INDEX(AvgFps), + INDEX(GPUDrv), + INDEX(Width), + INDEX(MapName) +); + +create table if not exists tf_perfdata_rollup_fps +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + INDEX(CreationTimeStamp), +); + +create table if not exists tf_perfdata_rollup_fpsdist +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + BaseFps FLOAT, + GPUDrv VARCHAR(64), + INDEX(CreationTimeStamp), + INDEX(GPUDrv) +); + +create table if not exists tf_perfdata_rollup_fpsmap +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + MapName VARCHAR(64), + INDEX(CreationTimeStamp), + INDEX(MapName), +); + +create table if not exists tf_perfdata_rollup_fpscard +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + GPUDrv VARCHAR(64), + INDEX(CreationTimeStamp), + INDEX(GPUDrv), +); + +create table if not exists tf_perfdata_rollup_fpswidth +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + Width INT, + INDEX(CreationTimeStamp), +); + +create table if not exists tf_perfdata_rollup_fpscardwidth +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + Width INT, + GPUDrv VARCHAR(64), + INDEX(CreationTimeStamp), + INDEX(GPUDrv), +); + +create table if not exists tf_perfdata_rollup_cpudist +( + CreationTimeStamp DATETIME, + NumEntries BIGINT, + AvgFps FLOAT, + CPUID VARCHAR(64), + NumCores INT, + BaseCPUGhz FLOAT, + GPUVendor INT, + INDEX(CreationTimeStamp), +); + +create table if not exists tf_perfdata_rollup_playtime +( + CreationTimeStamp DATETIME, + AvgSession FLOAT, + AvgLevels FLOAT, + INDEX(CreationTimeStamp), +); + +create table if not exists tf_perfdata_rollup_playtimedist +( + CreationTimeStamp DATETIME, + BaseSession FLOAT, + NumEntries INT, + INDEX(CreationTimeStamp), +); diff --git a/devtools/processgamestats/tf_gamestats_parse.cpp b/devtools/processgamestats/tf_gamestats_parse.cpp new file mode 100644 index 0000000..b8dd242 --- /dev/null +++ b/devtools/processgamestats/tf_gamestats_parse.cpp @@ -0,0 +1,480 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// +// + +#include "stdafx.h" +#include <stdio.h> +#include <process.h> +#include <string.h> +#include <windows.h> +#include <sys/stat.h> +#include <time.h> +#include "interface.h" +#include "imysqlwrapper.h" +#include "tier1/utlvector.h" +#include "tier1/utlbuffer.h" +#include "tier1/utlsymbol.h" +#include "tier1/utlstring.h" +#include "tier1/utldict.h" +#include "tier2/tier2.h" +#include "filesystem.h" + +#include "cbase.h" +#include "gamestats.h" +class CBaseObject; +#include "tf/tf_gamestats.h" +#include "base_gamestats_parse.h" +#include "tier0/icommandline.h" + +#include <string> + +static TFReportedStats_t g_reportedStats; + +extern void v_escape_string (std::string& s); + +//----------------------------------------------------------------------------- +// Weapons. +//----------------------------------------------------------------------------- +static const char *s_aWeaponNames[] = +{ + "NONE", + "BAT", + "BOTTLE", + "FIREAXE", + "CLUB", + "CROWBAR", + "KNIFE", + "MEDIKIT", + "PIPE", + "SHOVEL", + "WRENCH", + "BONESAW", + "SHOTGUN_PRIMARY", + "SHOTGUN_SECONDARY", + "SNIPERRIFLE", + "MINIGUN", + "SMG", + "SYRINGEGUN_MEDIC", + "TRANQ", + "ROCKETLAUNCHER", + "GRENADELAUNCHER", + "PIPEBOMBLAUNCHER", + "FLAMETHROWER", + "GRENADE_NORMAL", + "GRENADE_CONCUSSION", + "GRENADE_NAIL", + "GRENADE_MIRV", + "GRENADE_MIRV_DEMOMAN", + "GRENADE_NAPALM", + "GRENADE_GAS", + "GRENADE_EMP", + "GRENADE_CALTROP", + "GRENADE_PIPEBOMB", + "GRENADE_SMOKE_BOMB", + "GRENADE_HEAL", + "PISTOL", + "REVOLVER", + "NAILGUN", + "PDA", + "PDA_DEMOMAN", + "PDA_ENGINEER", + "PDA_SPY", + "BUILDER", + "MEDIGUN", + "FLAG", + "GRENADE_MIRVBOMB", + "FLAMETHROWER_ROCKET", + "GRENADE_DEMOMAN", + "SENTRY_BULLET", + "SENTRY_ROCKET", + "DISPENSER", + "INVIS", +}; + +//----------------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------------- +static const char *s_aClassNames[] = +{ + "UNDEFINED", + "SCOUT", + "SNIPER", + "SOLDIER", + "DEMOMAN", + "MEDIC", + "HEAVYWEAPONS", + "PYRO", + "SPY", + "ENGINEER", + "CIVILIAN", +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static bool LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer ) +{ + g_reportedStats.Clear(); + return g_reportedStats.LoadCustomDataFromBuffer( LoadBuffer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static const char *ClassIdToAlias( int iClass ) +{ + if ( ( iClass >= ARRAYSIZE( s_aClassNames ) ) || ( iClass < 0 ) ) + return NULL; + + return s_aClassNames[iClass]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *WeaponIdToAlias( int iWeapon ) +{ + if ( ( iWeapon >= ARRAYSIZE( s_aWeaponNames ) ) || ( iWeapon < 0 ) ) + return NULL; + + return s_aWeaponNames[iWeapon]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void DescribeTF2Stats() +{ +#if 0 // not up to date w/latest stats code. + int iMap; + for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) ) + { + // Get the current map. + TF_Gamestats_LevelStats_t *pCurrentMap = &g_dictMapStats[iMap]; + + Msg( " --- %s ------\n %d deaths\n %.2f seconds total playtime\n", pCurrentMap->m_Header.m_szMapName, + pCurrentMap->m_aPlayerDeaths.Count(), pCurrentMap->m_Header.m_flTime ); + for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ ) + { + Msg( " %s killed %s with %s at (%d,%d,%d), distance %d\n", + ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ), + ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ), + WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ), + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ], + pCurrentMap->m_aPlayerDeaths[ i ].iDistance ); + } + Msg( "\n---------------------------------\n\n %d damage records\n", pCurrentMap->m_aPlayerDamage.Count() ); + + for( int i = 0; i < pCurrentMap->m_aPlayerDamage.Count(); i++ ) + { + Msg( " %.2f : %s at (%d,%d,%d) caused %d damage to %s with %s at (%d,%d,%d)%s%s\n", + pCurrentMap->m_aPlayerDamage[ i ].fTime, + ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iAttackClass ), + pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 0 ], + pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 1 ], + pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 2 ], + pCurrentMap->m_aPlayerDamage[ i ].iDamage, + ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iTargetClass ), + WeaponIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iWeapon ), + pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 0 ], + pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 1 ], + pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 2 ], + pCurrentMap->m_aPlayerDamage[ i ].iCrit ? ", CRIT!" : "", + pCurrentMap->m_aPlayerDamage[ i ].iKill ? ", KILL" : "" ); + } + + Msg( "\n" ); + } +#endif +} +extern CUtlDict< int, unsigned short > g_mapOrder; +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void InsertTF2Data( bool bDeathOnly, char const *szStatsFileUserID, time_t fileTime, IMySQL *sql, int iStatsFileVersion ) +{ + if ( !sql ) + return; + + std::string userid; + userid = szStatsFileUserID; + v_escape_string( userid ); + + + char szDate[128]="Now()"; + if ( fileTime > 0 ) + { + tm * pTm = localtime( &fileTime ); + Q_snprintf( szDate, ARRAYSIZE( szDate ), "'%04d-%02d-%02d %02d:%02d:%02d'", + pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec ); + } + + char q[4096]; + + + { + int iMap; + for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex(); iMap = g_reportedStats.m_dictMapStats.Next( iMap ) ) + { + // Get the current map. + TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap]; + +#if 1 + int slot = g_mapOrder.Find( pCurrentMap->m_Header.m_szMapName ); + if ( slot == g_mapOrder.InvalidIndex() ) + { + if ( Q_stricmp( pCurrentMap->m_Header.m_szMapName, "devtest" ) ) + continue; + } +#endif + + std::string mapname; + mapname = pCurrentMap->m_Header.m_szMapName; + v_escape_string( mapname ); + + std::string tag; + tag = ""; // pCurrentMap->m_Tag.m_szTagText; + v_escape_string( tag ); + + int mapversion = 0; + + + + /* + for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ ) + { + Msg( " %s killed %s with %s at (%d,%d,%d), distance %d\n", + ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ), + ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ), + WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ), + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ], + pCurrentMap->m_aPlayerDeaths[ i ].iDistance ); + } + */ + // Deal with deaths + for ( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); ++i ) + { + Q_snprintf( q, sizeof( q ), "REPLACE into %s_deaths (UserID,Tag,LastUpdate,MapName,MapVersion,DeathIndex,X,Y,Z) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,%d,%d,%d,%d);", + "tf", + userid.c_str(), + tag.c_str(), + mapname.c_str(), + mapversion, + i, + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ], + pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ] ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + printf( "Query %s failed\n", q ); + return; + } + } + } + if ( bDeathOnly ) + return; + } + + int iMap; + for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex(); + iMap = g_reportedStats.m_dictMapStats.Next( iMap ) ) + { + // Get the current map. + TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap]; + std::string mapname = pCurrentMap->m_Header.m_szMapName; + v_escape_string( mapname ); + + // insert map data into database + Q_snprintf( q, sizeof( q ), "INSERT into tf_mapdata (ServerID,TimeSubmitted,MapName,RoundsPlayed,TotalTime,BlueWins,RedWins,Stalemates,BlueSuddenDeathWins,RedSuddenDeathWins) " + "values (\"%s\",%s,\"%s\",%d,%d,%d,%d,%d,%d,%d);", + szStatsFileUserID,szDate, mapname.c_str(), pCurrentMap->m_Header.m_iRoundsPlayed, pCurrentMap->m_Header.m_iTotalTime, pCurrentMap->m_Header.m_iBlueWins, + pCurrentMap->m_Header.m_iRedWins, pCurrentMap->m_Header.m_iStalemates, pCurrentMap->m_Header.m_iBlueSuddenDeathWins, pCurrentMap->m_Header.m_iRedSuddenDeathWins ); + +// Msg( "%s\n", q ); + + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + Msg( "Query %s failed\n", q ); + return; + } + + // insert the class data + for ( int i = TF_CLASS_UNDEFINED + 1; i < TF_CLASS_CIVILIAN; i++ ) + { + TF_Gamestats_ClassStats_t &classStats = pCurrentMap->m_aClassStats[i]; + if ( 0 == classStats.iSpawns ) // skip any classes that have had no spawns + continue; + + Q_snprintf( q, sizeof( q ), "INSERT into tf_classdata (ServerID,MapName,TimeSubmitted,Class,Spawns,TotalTime,Score,Kills,Deaths,Assists,Captures) " + "values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%d,%d);", + szStatsFileUserID, mapname.c_str(), szDate, i, classStats.iSpawns, classStats.iTotalTime, classStats.iScore, classStats.iKills, classStats.iDeaths, + classStats.iAssists, classStats.iCaptures ); + +// Msg( "%s\n", q ); + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + Msg( "Query %s failed\n", q ); + return; + } + } + + // insert the weapon data + for ( int i = TF_WEAPON_NONE+1; i < TF_WEAPON_COUNT; i++ ) + { + TF_Gamestats_WeaponStats_t &weaponStats = pCurrentMap->m_aWeaponStats[i]; + + // skip any weapons that have no shots fired + if ( 0 == weaponStats.iShotsFired ) + continue; + + Q_snprintf( q, sizeof( q ), "INSERT into tf_weapondata (ServerID, MapName, TimeSubmitted,WeaponID,ShotsFired,ShotsFiredCrit,ShotsHit,DamageTotal," + "HitsWithKnownDistance,DistanceTotal) " + "values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%llu)", + szStatsFileUserID, mapname.c_str(), szDate, i, weaponStats.iShotsFired, weaponStats.iCritShotsFired, weaponStats.iHits, weaponStats.iTotalDamage, + weaponStats.iHitsWithKnownDistance, weaponStats.iTotalDistance ); + +// Msg( "%s\n", q ); + int retcode = sql->Execute( q ); + if ( retcode != 0 ) + { + Msg( "Query %s failed\n", q ); + return; + } + } + } +} + +bool g_bDeathOnly = false; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int TF_ParseCustomGameStatsData( ParseContext_t *ctx ) +{ + FILE *fp = fopen( ctx->file, "rb" ); + if ( !fp ) + return CUSTOMDATA_FAILED; + + CUtlBuffer statsBuffer; + + struct _stat sb; + _stat( ctx->file, &sb ); + statsBuffer.Clear(); + statsBuffer.EnsureCapacity( sb.st_size ); + fread( statsBuffer.Base(), sb.st_size, 1, fp ); + statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size ); + fclose( fp ); + + if ( memcmp( statsBuffer.Base(), "\"gamestats\"", 11 ) == 0 ) + { + Msg( "Got new-style file format that we don't support, skipping\n" ); + return CUSTOMDATA_SUCCESS; + } + + if ( memcmp( statsBuffer.Base(), "PERFDATA:", 9 ) == 0 ) + { + ProcessPerfData( ctx->mysql, sb.st_mtime, "tf_perfdata", ( ( char * ) statsBuffer.Base() ) + 9 ); + return CUSTOMDATA_SUCCESS; + } + char shortname[ 128 ]; + Q_FileBase( ctx->file, shortname, sizeof( shortname ) ); + + char szCurrentStatsFileUserID[17]; + int iCurrentStatsFileVersion; + + iCurrentStatsFileVersion = statsBuffer.GetShort(); + statsBuffer.Get( szCurrentStatsFileUserID, 16 ); + szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0; + + unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt(); + if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED ) + { + // we don't care about the standard data, so why is it here? + return CUSTOMDATA_FAILED; + } + + // check for custom data + bool bHasCustomData = (statsBuffer.TellPut() != statsBuffer.TellGet()); + if( !bHasCustomData ) + { + // where's our data? + return CUSTOMDATA_FAILED; + } + + if ( !LoadCustomDataFromBuffer( statsBuffer ) ) + return CUSTOMDATA_FAILED; + + if( ctx->describeonly ) + { + DescribeTF2Stats(); + } + else + { + if ( CommandLine()->CheckParm( "-deathsonly" ) ) + { + g_bDeathOnly = true; + } + + InsertTF2Data( g_bDeathOnly, szCurrentStatsFileUserID, sb.st_mtime, ctx->mysql, iCurrentStatsFileVersion ); + } + + return CUSTOMDATA_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Purpose: Called after all new files have been parsed & imported +//----------------------------------------------------------------------------- +void TF_PostImport( IMySQL *sql ) +{ +#if 0 // now handled by PHP script + if ( g_bDeathOnly ) + return; + // All the new data files have been imported to SQL. Now, do a rollup of the raw data into the rollup tables. + + // delete existing rollup for class data + int retcode = sql->Execute( "delete from tf_classdata_rollup" ); + if ( 0 != retcode ) + { + Msg( "Failed to delete from tf_classdata_rollup\n" ); + return; + } + // create new rollup for class data + retcode = sql->Execute( "insert into tf_classdata_rollup (class,spawns,totaltime,score,kills,deaths,assists,captures) " + "select class,sum(spawns),sum(totaltime),sum(score),sum(kills),sum(deaths),sum(assists),sum(captures) from tf_classdata group by class;" ); + if ( 0 != retcode ) + { + Msg( "Failed to create class data rollup\n" ); + return; + } + + // delete existing rollup for map data + retcode = sql->Execute( "delete from tf_mapdata_rollup" ); + if ( 0 != retcode ) + { + Msg( "Failed to delete from tf_mapdata_rollup\n" ); + return; + } + // create new rollup for map data + retcode = sql->Execute( "insert into tf_mapdata_rollup (mapname,roundsplayed,totaltime,bluewins,redwins,stalemates) " + "select mapname,sum(roundsplayed),sum(totaltime),sum(bluewins),sum(redwins),sum(stalemates) from tf_mapdata group by mapname;" ); + if ( 0 != retcode ) + { + Msg( "Failed to create map data rollup\n" ); + return; + } +#endif +} |