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