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.
982 lines
25 KiB
982 lines
25 KiB
//========= 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) ); |
|
} |
|
|
|
|