summaryrefslogtreecommitdiff
path: root/devtools/processgamestats/processgamestats.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/processgamestats/processgamestats.cpp')
-rw-r--r--devtools/processgamestats/processgamestats.cpp982
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) );
+}
+