You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
14 KiB
480 lines
14 KiB
//========= 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 |
|
}
|
|
|