Team Fortress 2 Source Code as on 22/4/2020
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

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //
  7. //=============================================================================//
  8. // tagbuild.cpp : Defines the entry point for the console application.
  9. //
  10. #include "stdafx.h"
  11. #include <stdio.h>
  12. #include <process.h>
  13. #include <string.h>
  14. #include <windows.h>
  15. #include <sys/stat.h>
  16. #include <time.h>
  17. #include "interface.h"
  18. #include "imysqlwrapper.h"
  19. #include "tier1/utlvector.h"
  20. #include "tier1/utlbuffer.h"
  21. #include "tier1/utlsymbol.h"
  22. #include "tier1/utlstring.h"
  23. #include "tier1/utldict.h"
  24. #include "KeyValues.h"
  25. #include "filesystem_helpers.h"
  26. #include "tier2/tier2.h"
  27. #include "filesystem.h"
  28. #include "base_gamestats_parse.h"
  29. #include "cbase.h"
  30. #include "gamestats.h"
  31. #include "tier0/icommandline.h"
  32. // 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
  33. // only hold 64K entries. We sometimes need to process more than 64K files at a time.
  34. struct AnalysisData
  35. {
  36. AnalysisData()
  37. {
  38. symbols.SetLessFunc( CaselessStringLessThanIgnoreSlashes );
  39. }
  40. ~AnalysisData()
  41. {
  42. int i = symbols.FirstInorder();
  43. while ( i != symbols.InvalidIndex() )
  44. {
  45. const char *symbol = symbols[i];
  46. if ( symbol )
  47. {
  48. delete symbol;
  49. }
  50. i = symbols.NextInorder( i );
  51. }
  52. }
  53. CUtlRBTree<const char*,int> symbols;
  54. };
  55. static AnalysisData g_Analysis;
  56. static bool describeonly = false;
  57. typedef int (*DataParseFunc)( ParseContext_t * );
  58. typedef void (*PostImportFunc) ( IMySQL *sql );
  59. typedef bool (*ParseCurrentUserIDFunc)( char const *pchDataFile, char *pchUserID, size_t bufsize, time_t &modifiedtime );
  60. extern int CS_ParseCustomGameStatsData( ParseContext_t *ctx );
  61. extern int Ep2_ParseCustomGameStatsData( ParseContext_t *ctx );
  62. extern int TF_ParseCustomGameStatsData( ParseContext_t *ctx );
  63. extern void TF_PostImport( IMySQL *sql );
  64. int Default_ParseCustomGameStatsData( ParseContext_t *ctx );
  65. extern bool Ep2_ParseCurrentUserID( char const *pchDataFile, char *pchUserID, size_t bufsize, time_t &modifiedtime );
  66. struct DataParser_t
  67. {
  68. char const *pchGameName;
  69. DataParseFunc pfnParseFunc;
  70. PostImportFunc pfnPostImport;
  71. ParseCurrentUserIDFunc pfnParseUserID;
  72. };
  73. static DataParser_t g_ParseFuncs[] =
  74. {
  75. { "cstrike", CS_ParseCustomGameStatsData, NULL },
  76. { "tf", TF_ParseCustomGameStatsData, TF_PostImport },
  77. // { "dods", Default_ParseCustomGameStatsData, NULL },
  78. // { "portal", Default_ParseCustomGameStatsData, NULL },
  79. { "ep1", Default_ParseCustomGameStatsData, NULL }, // Just a STUB
  80. { "ep2", Ep2_ParseCustomGameStatsData, NULL, Ep2_ParseCurrentUserID }
  81. };
  82. //-----------------------------------------------------------------------------
  83. // Purpose:
  84. //-----------------------------------------------------------------------------
  85. void printusage( void )
  86. {
  87. printf( "processgamestats:\n" );
  88. printf( "processgamestats game dbhost user password dbname rootdir\n" );
  89. printf( "processgamestats game datafile [describe only]\n\n" );
  90. printf( "valid gamenames:\n" );
  91. for ( int i = 0 ; i < ARRAYSIZE( g_ParseFuncs ); ++i )
  92. {
  93. printf( " %s\n", g_ParseFuncs[ i ].pchGameName );
  94. }
  95. // Exit app
  96. exit( 1 );
  97. }
  98. void BuildFileList_R( CUtlVector< int >& files, char const *dir, char const *extension )
  99. {
  100. WIN32_FIND_DATA wfd;
  101. char directory[ 256 ];
  102. char filename[ 256 ];
  103. HANDLE ff;
  104. sprintf( directory, "%s\\*.*", dir );
  105. if ( ( ff = FindFirstFile( directory, &wfd ) ) == INVALID_HANDLE_VALUE )
  106. return;
  107. int extlen = strlen( extension );
  108. do
  109. {
  110. if ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY )
  111. {
  112. if ( wfd.cFileName[ 0 ] == '.' )
  113. continue;
  114. // Recurse down directory
  115. sprintf( filename, "%s\\%s", dir, wfd.cFileName );
  116. BuildFileList_R( files, filename, extension );
  117. }
  118. else
  119. {
  120. int len = strlen( wfd.cFileName );
  121. if ( len > extlen )
  122. {
  123. if ( !stricmp( &wfd.cFileName[ len - extlen ], extension ) )
  124. {
  125. char filename[ MAX_PATH ];
  126. Q_snprintf( filename, sizeof( filename ), "%s\\%s", dir, wfd.cFileName );
  127. _strlwr( filename );
  128. Q_FixSlashes( filename );
  129. char *symbol = strdup( filename );
  130. int sym = g_Analysis.symbols.Insert( symbol );
  131. files.AddToTail( sym );
  132. }
  133. }
  134. }
  135. } while ( FindNextFile( ff, &wfd ) );
  136. }
  137. void BuildFileList( CUtlVector< int >& files, char const *rootdir, char const *extension )
  138. {
  139. files.RemoveAll();
  140. BuildFileList_R( files, rootdir, extension );
  141. }
  142. void DescribeData( BasicGameStats_t &stats, const char *szStatsFileUserID, int iStatsFileVersion )
  143. {
  144. double averageSession = 0.0f;
  145. if ( stats.m_Summary.m_nCount > 0 )
  146. {
  147. averageSession = (double)stats.m_Summary.m_nSeconds / (double)stats.m_Summary.m_nCount;
  148. }
  149. Msg( "---------------------------------------------------------------------------\n" );
  150. Msg( "%16.16s : %s\n", "User", szStatsFileUserID );
  151. Msg( " %16.16s: %8d\n", "Blob version", iStatsFileVersion );
  152. Msg( " %16.16s: %8d sessions\n", "Played", stats.m_Summary.m_nCount );
  153. Msg( " %16.16s: %8d seconds\n", "Total Time", stats.m_Summary.m_nSeconds );
  154. Msg( " %16.16s: %8.2f seconds\n", "Avg Session", averageSession );
  155. Msg( " %16.16s: %8d\n", "Commentary", stats.m_Summary.m_nCommentary );
  156. Msg( " %16.16s: %8d\n", "HDR", stats.m_Summary.m_nHDR );
  157. Msg( " %16.16s: %8d\n", "Captions", stats.m_Summary.m_nCaptions );
  158. Msg( " %16.16s: %8d\n", "Easy", stats.m_Summary.m_nSkill[ 0 ] );
  159. Msg( " %16.16s: %8d\n", "Medium", stats.m_Summary.m_nSkill[ 1 ] );
  160. Msg( " %16.16s: %8d\n", "Hard", stats.m_Summary.m_nSkill[ 2 ] );
  161. Msg( " %16.16s: %8d seconds\n", "Completion time ", stats.m_nSecondsToCompleteGame );
  162. Msg( " %16.16s: %8d\n", "Number of deaths", stats.m_Summary.m_nDeaths );
  163. Msg( " -- Maps played --\n" );
  164. for ( int i = stats.m_MapTotals.First(); i != stats.m_MapTotals.InvalidIndex(); i = stats.m_MapTotals.Next( i ) )
  165. {
  166. char const *mapname = stats.m_MapTotals.GetElementName( i );
  167. BasicGameStatsRecord_t &rec = stats.m_MapTotals[ i ];
  168. Msg( " %16.16s: %5d seconds in %3d sessions (%4d deaths)\n", mapname, rec.m_nSeconds, rec.m_nCount, rec.m_nDeaths );
  169. }
  170. }
  171. #include <string>
  172. //-------------------------------------------------
  173. void v_escape_string (std::string& s)
  174. {
  175. if ( !s.size() )
  176. return;
  177. for ( unsigned int i = 0;i<s.size();i++ )
  178. {
  179. switch (s[i])
  180. {
  181. case '\0': /* Must be escaped for "mysql" */
  182. s[i] = '\\';
  183. s.insert(i+1,"0",1); i++;//lint !e534
  184. break;
  185. case '\n': /* Must be escaped for logs */
  186. s[i] = '\\';
  187. s.insert(i+1,"n",1); i++;//lint !e534
  188. break;
  189. case '\r':
  190. s[i] = '\\';
  191. s.insert(i+1,"r",1); i++;//lint !e534
  192. break;
  193. case '\\':
  194. s[i] = '\\';
  195. s.insert(i+1,"\\",1); i++;//lint !e534
  196. break;
  197. case '\"':
  198. s[i] = '\\';
  199. s.insert(i+1,"\"",1); i++;//lint !e534
  200. break;
  201. case '\'': /* Better safe than sorry */
  202. s[i] = '\\';
  203. s.insert(i+1,"\'",1); i++;//lint !e534
  204. break;
  205. case '\032': /* This gives problems on Win32 */
  206. s[i] = '\\';
  207. s.insert(i+1,"Z",1); i++;//lint !e534
  208. break;
  209. default:
  210. break;
  211. }
  212. }
  213. }
  214. void InsertData( CUtlDict< int, unsigned short >& mapOrder, IMySQL *sql, BasicGameStats_t &gs, const char *szStatsFileUserID, int iStatsFileVersion, const char *gamename, char const *tag = NULL )
  215. {
  216. if ( !sql )
  217. return;
  218. char q[ 512 ];
  219. std::string userid;
  220. userid = szStatsFileUserID;
  221. v_escape_string( userid );
  222. int farthestPlayed = -1;
  223. std::string highestmap;
  224. int namelen = 20;
  225. if ( !Q_stricmp( gamename, "ep1" ) )
  226. {
  227. namelen = 16;
  228. }
  229. char finalname[ 64 ];
  230. std::string finaltag;
  231. finaltag = tag ? tag : "";
  232. v_escape_string( finaltag );
  233. // Deal with the maps
  234. for ( int i = gs.m_MapTotals.First(); i != gs.m_MapTotals.InvalidIndex(); i = gs.m_MapTotals.Next( i ) )
  235. {
  236. char const *pszMapName = gs.m_MapTotals.GetElementName( i );
  237. std::string mapname;
  238. mapname = pszMapName;
  239. v_escape_string( mapname );
  240. Q_strncpy( finalname, mapname.c_str(), namelen );
  241. int slot = mapOrder.Find( pszMapName );
  242. if ( slot != mapOrder.InvalidIndex() )
  243. {
  244. int order = mapOrder[ slot ];
  245. if ( order > farthestPlayed )
  246. {
  247. farthestPlayed = order;
  248. }
  249. }
  250. else
  251. {
  252. if ( Q_stricmp( pszMapName, "devtest" ) )
  253. continue;
  254. }
  255. BasicGameStatsRecord_t& rec = gs.m_MapTotals[ i ];
  256. if ( tag )
  257. {
  258. 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);",
  259. gamename,
  260. userid.c_str(),
  261. iStatsFileVersion,
  262. finalname,
  263. finaltag.c_str(),
  264. rec.m_nCount,
  265. rec.m_nSeconds,
  266. rec.m_nHDR,
  267. rec.m_nCaptions,
  268. rec.m_nCommentary,
  269. rec.m_nSkill[ 0 ],
  270. rec.m_nSkill[ 1 ],
  271. rec.m_nSkill[ 2 ],
  272. rec.m_bSteam ? 0 : 1,
  273. rec.m_bCyberCafe ? 1 : 0,
  274. rec.m_nDeaths );
  275. }
  276. else
  277. {
  278. 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);",
  279. gamename,
  280. userid.c_str(),
  281. iStatsFileVersion,
  282. finalname,
  283. rec.m_nCount,
  284. rec.m_nSeconds,
  285. rec.m_nHDR,
  286. rec.m_nCaptions,
  287. rec.m_nCommentary,
  288. rec.m_nSkill[ 0 ],
  289. rec.m_nSkill[ 1 ],
  290. rec.m_nSkill[ 2 ],
  291. rec.m_bSteam ? 0 : 1,
  292. rec.m_bCyberCafe ? 1 : 0,
  293. rec.m_nDeaths );
  294. }
  295. int retcode = sql->Execute( q );
  296. if ( retcode != 0 )
  297. {
  298. printf( "Query %s failed\n", q );
  299. return;
  300. }
  301. }
  302. if ( farthestPlayed != -1 )
  303. {
  304. highestmap = mapOrder.GetElementName( farthestPlayed );
  305. }
  306. v_escape_string( highestmap );
  307. Q_strncpy( finalname, highestmap.c_str(), namelen );
  308. if ( tag )
  309. {
  310. 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);",
  311. gamename,
  312. userid.c_str(),
  313. iStatsFileVersion,
  314. finaltag.c_str(),
  315. gs.m_Summary.m_nCount,
  316. gs.m_Summary.m_nSeconds,
  317. gs.m_Summary.m_nHDR,
  318. gs.m_Summary.m_nCaptions,
  319. gs.m_Summary.m_nCommentary,
  320. gs.m_Summary.m_nSkill[ 0 ],
  321. gs.m_Summary.m_nSkill[ 1 ],
  322. gs.m_Summary.m_nSkill[ 2 ],
  323. gs.m_nSecondsToCompleteGame,
  324. finalname,
  325. gs.m_bSteam ? 0 : 1,
  326. gs.m_bCyberCafe ? 1 : 0,
  327. gs.m_nHL2ChaptureUnlocked,
  328. gs.m_nDXLevel,
  329. gs.m_Summary.m_nDeaths );
  330. }
  331. else
  332. {
  333. 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);",
  334. gamename,
  335. userid.c_str(),
  336. iStatsFileVersion,
  337. gs.m_Summary.m_nCount,
  338. gs.m_Summary.m_nSeconds,
  339. gs.m_Summary.m_nHDR,
  340. gs.m_Summary.m_nCaptions,
  341. gs.m_Summary.m_nCommentary,
  342. gs.m_Summary.m_nSkill[ 0 ],
  343. gs.m_Summary.m_nSkill[ 1 ],
  344. gs.m_Summary.m_nSkill[ 2 ],
  345. gs.m_nSecondsToCompleteGame,
  346. finalname,
  347. gs.m_bSteam ? 0 : 1,
  348. gs.m_bCyberCafe ? 1 : 0,
  349. gs.m_nHL2ChaptureUnlocked,
  350. gs.m_nDXLevel,
  351. gs.m_Summary.m_nDeaths );
  352. }
  353. int retcode = sql->Execute( q );
  354. if ( retcode != 0 )
  355. {
  356. printf( "Query %s failed\n", q );
  357. return;
  358. }
  359. }
  360. CUtlDict< int, unsigned short > g_mapOrder;
  361. void BuildMapList( void )
  362. {
  363. void *buffer = NULL;
  364. char *pFileList;
  365. FILE * pFile;
  366. pFile = fopen ("maplist.txt", "r");
  367. int i = 0;
  368. if ( pFile )
  369. {
  370. long lSize;
  371. // obtain file size.
  372. fseek (pFile , 0 , SEEK_END);
  373. lSize = ftell (pFile);
  374. rewind (pFile);
  375. // allocate memory to contain the whole file.
  376. buffer = (char*) malloc (lSize);
  377. if ( buffer != NULL )
  378. {
  379. // copy the file into the buffer.
  380. fread (buffer,1,lSize,pFile);
  381. pFileList = (char*)buffer;
  382. char szToken[1024];
  383. while ( 1 )
  384. {
  385. pFileList = ParseFile( pFileList, szToken, false );
  386. if ( pFileList == NULL )
  387. break;
  388. g_mapOrder.Insert( szToken, i );
  389. i++;
  390. }
  391. }
  392. fclose( pFile );
  393. free( buffer );
  394. }
  395. else
  396. {
  397. Msg( "Couldn't load maplist.txt for mod!!!\n" );
  398. }
  399. }
  400. int Default_ParseCustomGameStatsData( ParseContext_t *ctx )
  401. {
  402. FILE *fp = fopen( ctx->file, "rb" );
  403. if ( fp )
  404. {
  405. CUtlBuffer statsBuffer;
  406. struct _stat sb;
  407. _stat( ctx->file, &sb );
  408. statsBuffer.Clear();
  409. statsBuffer.EnsureCapacity( sb.st_size );
  410. fread( statsBuffer.Base(), sb.st_size, 1, fp );
  411. statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size );
  412. fclose( fp );
  413. char shortname[ 128 ];
  414. Q_FileBase( ctx->file, shortname, sizeof( shortname ) );
  415. char szCurrentStatsFileUserID[17];
  416. int iCurrentStatsFileVersion;
  417. iCurrentStatsFileVersion = statsBuffer.GetShort();
  418. statsBuffer.Get( szCurrentStatsFileUserID, 16 );
  419. szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0;
  420. bool valid = true;
  421. unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt();
  422. if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED )
  423. {
  424. //standard data was saved, rewind so the stats can read in time to completion
  425. statsBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, -((int)sizeof( unsigned int )) );
  426. BasicGameStats_t stats;
  427. valid = stats.ParseFromBuffer( statsBuffer, iCurrentStatsFileVersion );
  428. if ( describeonly )
  429. {
  430. DescribeData( stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion );
  431. }
  432. else
  433. {
  434. if ( valid )
  435. {
  436. InsertData( g_mapOrder, ctx->mysql, stats, szCurrentStatsFileUserID, iCurrentStatsFileVersion, ctx->gamename );
  437. }
  438. else
  439. {
  440. ++ctx->skipcount;
  441. }
  442. }
  443. }
  444. //check for custom data
  445. bool bHasCustomData = (valid && (statsBuffer.TellPut() != statsBuffer.TellGet()));
  446. if( bHasCustomData )
  447. {
  448. if( describeonly )
  449. {
  450. //separate out the custom data and store it off for processing by other applications,
  451. //since they only wanted to 'describe' the data, just use a local temp and overwrite it each time
  452. const char *szCustomDataOutputFileName = "customdata_temp.dat";
  453. Msg( "\n\nFound custom data, dumping to %s\n", szCustomDataOutputFileName );
  454. FILE *pCustomDataOutput = fopen( szCustomDataOutputFileName, "wb+" );
  455. if( pCustomDataOutput )
  456. {
  457. int iGetPosition = statsBuffer.TellGet();
  458. fwrite( (((unsigned char *)statsBuffer.Base()) + iGetPosition), statsBuffer.TellPut() - iGetPosition, 1, pCustomDataOutput );
  459. fclose( pCustomDataOutput );
  460. }
  461. }
  462. else
  463. {
  464. //separate out the custom data and store it off for processing by other applications,
  465. //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
  466. if( ctx->bCustomDirectoryNotMade )
  467. {
  468. CreateDirectory( "customdatadumps", NULL );
  469. ctx->bCustomDirectoryNotMade = false;
  470. }
  471. char szCustomDataOutputFileName[256];
  472. Q_snprintf( szCustomDataOutputFileName, sizeof( szCustomDataOutputFileName ), "customdatadumps/%s.dat", szCurrentStatsFileUserID );
  473. FILE *pCustomDataOutput = fopen( szCustomDataOutputFileName, "wb+" );
  474. if( pCustomDataOutput )
  475. {
  476. int iGetPosition = statsBuffer.TellGet();
  477. fwrite( (((unsigned char *)statsBuffer.Base()) + iGetPosition), statsBuffer.TellPut() - iGetPosition, 1, pCustomDataOutput );
  478. fclose( pCustomDataOutput );
  479. }
  480. }
  481. }
  482. }
  483. return CUSTOMDATA_SUCCESS;
  484. }
  485. int main(int argc, char* argv[])
  486. {
  487. CommandLine()->CreateCmdLine( argc, argv );
  488. ParseContext_t ctx;
  489. if ( argc < 7 && argc != 3 )
  490. {
  491. printusage();
  492. }
  493. describeonly = argc == 3;
  494. int gameArg = 1;
  495. int hostArg = 2;
  496. int usernameArg = 3;
  497. int pwArg = 4;
  498. int dbArg = 5;
  499. int dirArg = 6;
  500. if ( describeonly )
  501. {
  502. dirArg = 2;
  503. }
  504. InitDefaultFileSystem();
  505. BuildMapList();
  506. const char *gamename = argv[ gameArg ];
  507. DataParseFunc parseFunc = NULL;
  508. PostImportFunc postImportFunc = NULL;
  509. ParseCurrentUserIDFunc parseUserIDFunc = NULL;
  510. for ( int i = 0 ; i < ARRAYSIZE( g_ParseFuncs ); ++i )
  511. {
  512. if ( !Q_stricmp( g_ParseFuncs[ i ].pchGameName, gamename ) )
  513. {
  514. parseFunc = g_ParseFuncs[ i ].pfnParseFunc;
  515. postImportFunc = g_ParseFuncs[ i ].pfnPostImport;
  516. parseUserIDFunc = g_ParseFuncs[ i ].pfnParseUserID;
  517. break;
  518. }
  519. }
  520. if ( !parseFunc )
  521. {
  522. printf( "Invalid game name '%s'\n", gamename );
  523. printusage();
  524. }
  525. bool batchMode = true;
  526. CUtlVector< int > files;
  527. if ( describeonly || Q_stristr( argv[ dirArg ], ".dat" ) )
  528. {
  529. char filename[ MAX_PATH ];
  530. Q_snprintf( filename, sizeof( filename ), "%s", argv[ dirArg ] );
  531. _strlwr( filename );
  532. Q_FixSlashes( filename );
  533. char *symbol = strdup( filename );
  534. int sym = g_Analysis.symbols.Insert( symbol );
  535. files.AddToTail( sym );
  536. batchMode = false;
  537. }
  538. else
  539. {
  540. Msg( "Building file list\n" );
  541. BuildFileList( files, argv[ dirArg ], "dat" );
  542. }
  543. if ( !files.Count() )
  544. {
  545. printf( "No files to operate upon\n" );
  546. exit( -1 );
  547. }
  548. int c = files.Count();
  549. // Cull list of files by looking for most recent version of user's stats and only keeping around those files
  550. if ( parseUserIDFunc )
  551. {
  552. struct CUserIDFileMapping
  553. {
  554. CUserIDFileMapping() :
  555. filename( UTL_INVAL_SYMBOL ), filemodifiedtime( 0 ), modcount( 1 )
  556. {
  557. userid[ 0 ] = 0;
  558. }
  559. char userid[ 17 ];
  560. CUtlSymbol filename;
  561. time_t filemodifiedtime;
  562. int modcount;
  563. static bool Less( const CUserIDFileMapping &lhs, const CUserIDFileMapping &rhs )
  564. {
  565. return Q_stricmp( lhs.userid, rhs.userid ) < 0;
  566. }
  567. };
  568. CUtlRBTree< CUserIDFileMapping, int > userIDToFileMap( 0, 0, CUserIDFileMapping::Less );
  569. int nDiscards = 0;
  570. int nSkips =0;
  571. int nMaxMod = 1;
  572. for ( int i = 0; i < c; ++i )
  573. {
  574. char const *fn = g_Analysis.symbols.Element( files[ i ] );
  575. CUserIDFileMapping search;
  576. search.filename = files[ i ];
  577. if ( (*parseUserIDFunc)( fn, search.userid, sizeof( search.userid ), search.filemodifiedtime ) )
  578. {
  579. // Find map index
  580. int idx = userIDToFileMap.Find( search );
  581. if ( idx == userIDToFileMap.InvalidIndex() )
  582. {
  583. userIDToFileMap.Insert( search );
  584. }
  585. else
  586. {
  587. CUserIDFileMapping &update = userIDToFileMap[ idx ];
  588. if ( search.filemodifiedtime > update.filemodifiedtime )
  589. {
  590. update.filename = files[ i ];
  591. update.filemodifiedtime = search.filemodifiedtime;
  592. update.modcount++;
  593. if ( update.modcount > nMaxMod )
  594. {
  595. nMaxMod = update.modcount;
  596. }
  597. }
  598. ++nDiscards;
  599. }
  600. }
  601. else
  602. {
  603. ++nSkips;
  604. }
  605. if ( i > 0 && !( i % 100 ) )
  606. {
  607. printf( "Parsing user ID's: [%-6.6d/%-6.6d] %.2f %% complete\n", i, c, 100.0f * (float)i/(float)c );
  608. }
  609. }
  610. Msg( "discarded %d of %d, remainder %d [%d skipped] max mod %d\n", nDiscards, c, userIDToFileMap.Count(), nSkips, nMaxMod );
  611. // Now re-write files and count with pared down listing
  612. files.Purge();
  613. for( int i = userIDToFileMap.FirstInorder(); i != userIDToFileMap.InvalidIndex(); i = userIDToFileMap.NextInorder( i ) )
  614. {
  615. files.AddToTail( userIDToFileMap[ i ].filename );
  616. }
  617. c = files.Count();
  618. }
  619. bool bTrySql = !describeonly;
  620. bool bSqlOkay = false;
  621. CSysModule *sql = NULL;
  622. CreateInterfaceFn factory = NULL;
  623. IMySQL *mysql = NULL;
  624. if ( bTrySql )
  625. {
  626. // Now connect to steamweb and update the engineaccess table
  627. sql = Sys_LoadModule( "mysql_wrapper" );
  628. if ( sql )
  629. {
  630. factory = Sys_GetFactory( sql );
  631. if ( factory )
  632. {
  633. mysql = ( IMySQL * )factory( MYSQL_WRAPPER_VERSION_NAME, NULL );
  634. if ( mysql )
  635. {
  636. if ( mysql->InitMySQL( argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ], argv[ pwArg ] ) )
  637. {
  638. bSqlOkay = true;
  639. if ( batchMode )
  640. {
  641. Msg( "Successfully connected to database %s on host %s, user %s\n",
  642. argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ] );
  643. }
  644. }
  645. else
  646. {
  647. Msg( "mysql->InitMySQL( %s, %s, %s, [password]) failed\n",
  648. argv[ dbArg ], argv[ hostArg ], argv[ usernameArg ] );
  649. }
  650. }
  651. else
  652. {
  653. Msg( "Unable to get MYSQL_WRAPPER_VERSION_NAME(%s) from mysql_wrapper\n", MYSQL_WRAPPER_VERSION_NAME );
  654. }
  655. }
  656. else
  657. {
  658. Msg( "Sys_GetFactory on mysql_wrapper failed\n" );
  659. }
  660. }
  661. else
  662. {
  663. Msg( "Sys_LoadModule( mysql_wrapper ) failed\n" );
  664. }
  665. }
  666. ctx.gamename = gamename;
  667. ctx.describeonly = describeonly;
  668. ctx.mysql = mysql;
  669. ctx.skipcount = 0;
  670. ctx.bCustomDirectoryNotMade = true;
  671. if ( bSqlOkay || describeonly )
  672. {
  673. for ( int i = 0; i < c; ++i )
  674. {
  675. char const *fn = g_Analysis.symbols.Element( files[ i ] );
  676. ctx.file = fn;
  677. int iCustomData = (*parseFunc)( &ctx );
  678. if ( iCustomData == CUSTOMDATA_SUCCESS )
  679. {
  680. if ( i > 0 && !( i % 100 ) )
  681. {
  682. printf( "Processing: [%-6.6d/%-6.6d] %.2f %% complete\n", i, c, 100.0f * (float)i/(float)c );
  683. }
  684. }
  685. }
  686. if ( ctx.skipcount > 0 )
  687. {
  688. printf( "Skipped %d samples which appear to be malformed or contain bogus data\n", ctx.skipcount );
  689. }
  690. // if this game has a post-import function to call after all the files have been imported, call it now
  691. if ( bSqlOkay && postImportFunc )
  692. {
  693. postImportFunc( mysql );
  694. }
  695. }
  696. if ( bSqlOkay )
  697. {
  698. if ( mysql )
  699. {
  700. mysql->Release();
  701. mysql = NULL;
  702. }
  703. if ( sql )
  704. {
  705. Sys_UnloadModule( sql );
  706. sql = NULL;
  707. }
  708. }
  709. return 0;
  710. }
  711. static void OverWriteCharsWeHate( char *pStr )
  712. {
  713. while( *pStr )
  714. {
  715. switch( *pStr )
  716. {
  717. case '\n':
  718. case '\r':
  719. case '\\':
  720. case '\"':
  721. case '\'':
  722. case '\032':
  723. case ';':
  724. *pStr = ' ';
  725. }
  726. pStr++;
  727. }
  728. }
  729. void InsertKeyDataIntoTable( IMySQL *pSQL, time_t fileTime, char const *pTableName, char const *pPerfData, char const *pKeyWhiteList[], int nNumFields )
  730. {
  731. char szDate[128]="Now()";
  732. if ( fileTime > 0 )
  733. {
  734. tm * pTm = localtime( &fileTime );
  735. Q_snprintf( szDate, ARRAYSIZE( szDate ), "'%04d-%02d-%02d %02d:%02d:%02d'",
  736. pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec );
  737. }
  738. // we don't need to worry about semicolons embedded in string fields because we supressed them
  739. // on the client. if some malicious person inserts them, the mandled field names will fail the
  740. // whitelist check, causing the record to be ignored.
  741. CUtlVector<char *> tokens;
  742. // split into tokens at non-quoated spaces or ;'s
  743. for(;;)
  744. {
  745. char const *pStr = pPerfData;
  746. if ( pStr[0] == 0 )
  747. break;
  748. while( pStr[0] && ( pStr[0] != ' ' ) && ( pStr[0] != ';' ) )
  749. {
  750. if ( pStr[0]=='"')
  751. {
  752. // skip to end quote
  753. char const *pEq = strchr( pStr + 1, '\"' );
  754. if ( ! pEq )
  755. {
  756. printf(" close quote with no open quote\n" );
  757. return;
  758. }
  759. pStr = pEq;
  760. }
  761. pStr++;
  762. }
  763. // got a field
  764. int nlen = pStr - pPerfData;
  765. if ( nlen > 2 )
  766. {
  767. char *pToken = new char[ nlen + 1 ];
  768. memcpy( pToken, pPerfData, nlen );
  769. pToken[nlen] = 0;
  770. tokens.AddToTail( pToken );
  771. }
  772. if ( pStr[0] )
  773. pStr++;
  774. pPerfData = pStr;
  775. }
  776. bool bBadData = false;
  777. char fieldNameBuffer[1024];
  778. char fieldValueBuffer[2048];
  779. strcpy( fieldNameBuffer, "(CreationTimeStamp, " );
  780. Q_snprintf( fieldValueBuffer, ARRAYSIZE( fieldValueBuffer), "( %s,", szDate );
  781. for( int i = 0; i < tokens.Count(); i++ )
  782. {
  783. char *pKVData = tokens[i];
  784. char *pEqualsSign = strchr( pKVData, '=' );
  785. if (! pEqualsSign )
  786. {
  787. bBadData = true;
  788. break;
  789. }
  790. *pEqualsSign = 0; // *semicolon->null
  791. // check that the field is in the white list
  792. bool bFoundIt = false;
  793. for( int nCheck = 0; nCheck < nNumFields; nCheck++ )
  794. if ( strcmp( pKVData, pKeyWhiteList[nCheck] ) == 0 )
  795. {
  796. bFoundIt = true;
  797. break;
  798. }
  799. V_strncat( fieldNameBuffer, pKVData, sizeof( fieldNameBuffer ) );
  800. if ( i != tokens.Count() -1 )
  801. V_strncat( fieldNameBuffer, ",", sizeof( fieldNameBuffer ) );
  802. else
  803. V_strncat( fieldNameBuffer, ")", sizeof( fieldNameBuffer ) );
  804. char *pValue = pEqualsSign + 1;
  805. OverWriteCharsWeHate( pValue );
  806. if ( ( strlen( pValue ) < 1 ) || (! bFoundIt ) )
  807. {
  808. bBadData = true;
  809. break;
  810. }
  811. // kill lead + trail space
  812. if ( pValue[0] == ' ' )
  813. pValue++;
  814. if ( pValue[strlen(pValue) - 1 ] == ' ' )
  815. pValue[strlen( pValue ) - 1 ] =0;
  816. V_strncat( fieldValueBuffer, "'", sizeof( fieldValueBuffer ) );
  817. V_strncat( fieldValueBuffer, pValue, sizeof( fieldValueBuffer ) );
  818. if ( i != tokens.Count() -1 )
  819. V_strncat( fieldValueBuffer, "',", sizeof( fieldValueBuffer ) );
  820. else
  821. V_strncat( fieldValueBuffer, "')", sizeof( fieldValueBuffer ) );
  822. }
  823. if (! bBadData )
  824. {
  825. char sqlCommandBuffer[1024 + sizeof( fieldNameBuffer ) + sizeof( fieldValueBuffer ) ];
  826. sprintf( sqlCommandBuffer, "insert into %s %s values %s;", pTableName, fieldNameBuffer, fieldValueBuffer );
  827. // printf("cmd %s\n", sqlCommandBuffer);
  828. int retcode = pSQL->Execute( sqlCommandBuffer );
  829. if ( retcode != 0 )
  830. {
  831. printf( "command %s failed\n", sqlCommandBuffer );
  832. }
  833. }
  834. tokens.PurgeAndDeleteElements();
  835. }
  836. char const *s_PerfKeyList[] = {
  837. "AvgFps",
  838. "MinFps",
  839. "MaxFps",
  840. "CPUID",
  841. "CPUGhz",
  842. "NumCores",
  843. "GPUDrv",
  844. "GPUVendor",
  845. "GPUDeviceID",
  846. "GPUDriverVersion",
  847. "DxLvl",
  848. "Width",
  849. "Height",
  850. "MapName",
  851. "TotalLevelTime",
  852. "NumLevels"
  853. };
  854. void ProcessPerfData( IMySQL *pSQL, time_t fileTime, char const *pTableName, char const *pPerfData )
  855. {
  856. InsertKeyDataIntoTable( pSQL, fileTime, pTableName, pPerfData, s_PerfKeyList, ARRAYSIZE( s_PerfKeyList) );
  857. }