//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// // tagbuild.cpp : Defines the entry point for the console application. // #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 "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 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 //------------------------------------------------- void v_escape_string (std::string& s) { if ( !s.size() ) return; for ( unsigned int i = 0;i& 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 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) ); }