//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// // // #include "stdafx.h" #include #include #include #include #include #include #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 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 }