diff options
Diffstat (limited to 'devtools/processgamestats/processgamestats.cpp')
| -rw-r--r-- | devtools/processgamestats/processgamestats.cpp | 982 |
1 files changed, 982 insertions, 0 deletions
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) ); +} + |