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.

480 lines
14 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //
  7. //=============================================================================//
  8. //
  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 "tier2/tier2.h"
  25. #include "filesystem.h"
  26. #include "cbase.h"
  27. #include "gamestats.h"
  28. class CBaseObject;
  29. #include "tf/tf_gamestats.h"
  30. #include "base_gamestats_parse.h"
  31. #include "tier0/icommandline.h"
  32. #include <string>
  33. static TFReportedStats_t g_reportedStats;
  34. extern void v_escape_string (std::string& s);
  35. //-----------------------------------------------------------------------------
  36. // Weapons.
  37. //-----------------------------------------------------------------------------
  38. static const char *s_aWeaponNames[] =
  39. {
  40. "NONE",
  41. "BAT",
  42. "BOTTLE",
  43. "FIREAXE",
  44. "CLUB",
  45. "CROWBAR",
  46. "KNIFE",
  47. "MEDIKIT",
  48. "PIPE",
  49. "SHOVEL",
  50. "WRENCH",
  51. "BONESAW",
  52. "SHOTGUN_PRIMARY",
  53. "SHOTGUN_SECONDARY",
  54. "SNIPERRIFLE",
  55. "MINIGUN",
  56. "SMG",
  57. "SYRINGEGUN_MEDIC",
  58. "TRANQ",
  59. "ROCKETLAUNCHER",
  60. "GRENADELAUNCHER",
  61. "PIPEBOMBLAUNCHER",
  62. "FLAMETHROWER",
  63. "GRENADE_NORMAL",
  64. "GRENADE_CONCUSSION",
  65. "GRENADE_NAIL",
  66. "GRENADE_MIRV",
  67. "GRENADE_MIRV_DEMOMAN",
  68. "GRENADE_NAPALM",
  69. "GRENADE_GAS",
  70. "GRENADE_EMP",
  71. "GRENADE_CALTROP",
  72. "GRENADE_PIPEBOMB",
  73. "GRENADE_SMOKE_BOMB",
  74. "GRENADE_HEAL",
  75. "PISTOL",
  76. "REVOLVER",
  77. "NAILGUN",
  78. "PDA",
  79. "PDA_DEMOMAN",
  80. "PDA_ENGINEER",
  81. "PDA_SPY",
  82. "BUILDER",
  83. "MEDIGUN",
  84. "FLAG",
  85. "GRENADE_MIRVBOMB",
  86. "FLAMETHROWER_ROCKET",
  87. "GRENADE_DEMOMAN",
  88. "SENTRY_BULLET",
  89. "SENTRY_ROCKET",
  90. "DISPENSER",
  91. "INVIS",
  92. };
  93. //-----------------------------------------------------------------------------
  94. // Classes
  95. //-----------------------------------------------------------------------------
  96. static const char *s_aClassNames[] =
  97. {
  98. "UNDEFINED",
  99. "SCOUT",
  100. "SNIPER",
  101. "SOLDIER",
  102. "DEMOMAN",
  103. "MEDIC",
  104. "HEAVYWEAPONS",
  105. "PYRO",
  106. "SPY",
  107. "ENGINEER",
  108. "CIVILIAN",
  109. };
  110. //-----------------------------------------------------------------------------
  111. // Purpose:
  112. //-----------------------------------------------------------------------------
  113. static bool LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer )
  114. {
  115. g_reportedStats.Clear();
  116. return g_reportedStats.LoadCustomDataFromBuffer( LoadBuffer );
  117. }
  118. //-----------------------------------------------------------------------------
  119. // Purpose:
  120. //-----------------------------------------------------------------------------
  121. static const char *ClassIdToAlias( int iClass )
  122. {
  123. if ( ( iClass >= ARRAYSIZE( s_aClassNames ) ) || ( iClass < 0 ) )
  124. return NULL;
  125. return s_aClassNames[iClass];
  126. }
  127. //-----------------------------------------------------------------------------
  128. // Purpose:
  129. //-----------------------------------------------------------------------------
  130. const char *WeaponIdToAlias( int iWeapon )
  131. {
  132. if ( ( iWeapon >= ARRAYSIZE( s_aWeaponNames ) ) || ( iWeapon < 0 ) )
  133. return NULL;
  134. return s_aWeaponNames[iWeapon];
  135. }
  136. //-----------------------------------------------------------------------------
  137. // Purpose:
  138. //-----------------------------------------------------------------------------
  139. void DescribeTF2Stats()
  140. {
  141. #if 0 // not up to date w/latest stats code.
  142. int iMap;
  143. for ( iMap = g_dictMapStats.First(); iMap != g_dictMapStats.InvalidIndex(); iMap = g_dictMapStats.Next( iMap ) )
  144. {
  145. // Get the current map.
  146. TF_Gamestats_LevelStats_t *pCurrentMap = &g_dictMapStats[iMap];
  147. Msg( " --- %s ------\n %d deaths\n %.2f seconds total playtime\n", pCurrentMap->m_Header.m_szMapName,
  148. pCurrentMap->m_aPlayerDeaths.Count(), pCurrentMap->m_Header.m_flTime );
  149. for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ )
  150. {
  151. Msg( " %s killed %s with %s at (%d,%d,%d), distance %d\n",
  152. ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ),
  153. ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ),
  154. WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ),
  155. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
  156. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
  157. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ],
  158. pCurrentMap->m_aPlayerDeaths[ i ].iDistance );
  159. }
  160. Msg( "\n---------------------------------\n\n %d damage records\n", pCurrentMap->m_aPlayerDamage.Count() );
  161. for( int i = 0; i < pCurrentMap->m_aPlayerDamage.Count(); i++ )
  162. {
  163. Msg( " %.2f : %s at (%d,%d,%d) caused %d damage to %s with %s at (%d,%d,%d)%s%s\n",
  164. pCurrentMap->m_aPlayerDamage[ i ].fTime,
  165. ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iAttackClass ),
  166. pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 0 ],
  167. pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 1 ],
  168. pCurrentMap->m_aPlayerDamage[ i ].nAttackerPosition[ 2 ],
  169. pCurrentMap->m_aPlayerDamage[ i ].iDamage,
  170. ClassIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iTargetClass ),
  171. WeaponIdToAlias( pCurrentMap->m_aPlayerDamage[ i ].iWeapon ),
  172. pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 0 ],
  173. pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 1 ],
  174. pCurrentMap->m_aPlayerDamage[ i ].nTargetPosition[ 2 ],
  175. pCurrentMap->m_aPlayerDamage[ i ].iCrit ? ", CRIT!" : "",
  176. pCurrentMap->m_aPlayerDamage[ i ].iKill ? ", KILL" : "" );
  177. }
  178. Msg( "\n" );
  179. }
  180. #endif
  181. }
  182. extern CUtlDict< int, unsigned short > g_mapOrder;
  183. //-----------------------------------------------------------------------------
  184. // Purpose:
  185. //-----------------------------------------------------------------------------
  186. void InsertTF2Data( bool bDeathOnly, char const *szStatsFileUserID, time_t fileTime, IMySQL *sql, int iStatsFileVersion )
  187. {
  188. if ( !sql )
  189. return;
  190. std::string userid;
  191. userid = szStatsFileUserID;
  192. v_escape_string( userid );
  193. char szDate[128]="Now()";
  194. if ( fileTime > 0 )
  195. {
  196. tm * pTm = localtime( &fileTime );
  197. Q_snprintf( szDate, ARRAYSIZE( szDate ), "'%04d-%02d-%02d %02d:%02d:%02d'",
  198. pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec );
  199. }
  200. char q[4096];
  201. {
  202. int iMap;
  203. for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex(); iMap = g_reportedStats.m_dictMapStats.Next( iMap ) )
  204. {
  205. // Get the current map.
  206. TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap];
  207. #if 1
  208. int slot = g_mapOrder.Find( pCurrentMap->m_Header.m_szMapName );
  209. if ( slot == g_mapOrder.InvalidIndex() )
  210. {
  211. if ( Q_stricmp( pCurrentMap->m_Header.m_szMapName, "devtest" ) )
  212. continue;
  213. }
  214. #endif
  215. std::string mapname;
  216. mapname = pCurrentMap->m_Header.m_szMapName;
  217. v_escape_string( mapname );
  218. std::string tag;
  219. tag = ""; // pCurrentMap->m_Tag.m_szTagText;
  220. v_escape_string( tag );
  221. int mapversion = 0;
  222. /*
  223. for( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ )
  224. {
  225. Msg( " %s killed %s with %s at (%d,%d,%d), distance %d\n",
  226. ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iAttackClass ),
  227. ClassIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iTargetClass ),
  228. WeaponIdToAlias( pCurrentMap->m_aPlayerDeaths[ i ].iWeapon ),
  229. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
  230. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
  231. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ],
  232. pCurrentMap->m_aPlayerDeaths[ i ].iDistance );
  233. }
  234. */
  235. // Deal with deaths
  236. for ( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); ++i )
  237. {
  238. Q_snprintf( q, sizeof( q ), "REPLACE into %s_deaths (UserID,Tag,LastUpdate,MapName,MapVersion,DeathIndex,X,Y,Z) values (\"%s\",\"%s\",Now(),\"%-.20s\",%d,%d,%d,%d,%d);",
  239. "tf",
  240. userid.c_str(),
  241. tag.c_str(),
  242. mapname.c_str(),
  243. mapversion,
  244. i,
  245. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 0 ],
  246. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 1 ],
  247. pCurrentMap->m_aPlayerDeaths[ i ].nPosition[ 2 ] );
  248. int retcode = sql->Execute( q );
  249. if ( retcode != 0 )
  250. {
  251. printf( "Query %s failed\n", q );
  252. return;
  253. }
  254. }
  255. }
  256. if ( bDeathOnly )
  257. return;
  258. }
  259. int iMap;
  260. for ( iMap = g_reportedStats.m_dictMapStats.First(); iMap != g_reportedStats.m_dictMapStats.InvalidIndex();
  261. iMap = g_reportedStats.m_dictMapStats.Next( iMap ) )
  262. {
  263. // Get the current map.
  264. TF_Gamestats_LevelStats_t *pCurrentMap = &g_reportedStats.m_dictMapStats[iMap];
  265. std::string mapname = pCurrentMap->m_Header.m_szMapName;
  266. v_escape_string( mapname );
  267. // insert map data into database
  268. Q_snprintf( q, sizeof( q ), "INSERT into tf_mapdata (ServerID,TimeSubmitted,MapName,RoundsPlayed,TotalTime,BlueWins,RedWins,Stalemates,BlueSuddenDeathWins,RedSuddenDeathWins) "
  269. "values (\"%s\",%s,\"%s\",%d,%d,%d,%d,%d,%d,%d);",
  270. szStatsFileUserID,szDate, mapname.c_str(), pCurrentMap->m_Header.m_iRoundsPlayed, pCurrentMap->m_Header.m_iTotalTime, pCurrentMap->m_Header.m_iBlueWins,
  271. pCurrentMap->m_Header.m_iRedWins, pCurrentMap->m_Header.m_iStalemates, pCurrentMap->m_Header.m_iBlueSuddenDeathWins, pCurrentMap->m_Header.m_iRedSuddenDeathWins );
  272. // Msg( "%s\n", q );
  273. int retcode = sql->Execute( q );
  274. if ( retcode != 0 )
  275. {
  276. Msg( "Query %s failed\n", q );
  277. return;
  278. }
  279. // insert the class data
  280. for ( int i = TF_CLASS_UNDEFINED + 1; i < TF_CLASS_CIVILIAN; i++ )
  281. {
  282. TF_Gamestats_ClassStats_t &classStats = pCurrentMap->m_aClassStats[i];
  283. if ( 0 == classStats.iSpawns ) // skip any classes that have had no spawns
  284. continue;
  285. Q_snprintf( q, sizeof( q ), "INSERT into tf_classdata (ServerID,MapName,TimeSubmitted,Class,Spawns,TotalTime,Score,Kills,Deaths,Assists,Captures) "
  286. "values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%d,%d);",
  287. szStatsFileUserID, mapname.c_str(), szDate, i, classStats.iSpawns, classStats.iTotalTime, classStats.iScore, classStats.iKills, classStats.iDeaths,
  288. classStats.iAssists, classStats.iCaptures );
  289. // Msg( "%s\n", q );
  290. int retcode = sql->Execute( q );
  291. if ( retcode != 0 )
  292. {
  293. Msg( "Query %s failed\n", q );
  294. return;
  295. }
  296. }
  297. // insert the weapon data
  298. for ( int i = TF_WEAPON_NONE+1; i < TF_WEAPON_COUNT; i++ )
  299. {
  300. TF_Gamestats_WeaponStats_t &weaponStats = pCurrentMap->m_aWeaponStats[i];
  301. // skip any weapons that have no shots fired
  302. if ( 0 == weaponStats.iShotsFired )
  303. continue;
  304. Q_snprintf( q, sizeof( q ), "INSERT into tf_weapondata (ServerID, MapName, TimeSubmitted,WeaponID,ShotsFired,ShotsFiredCrit,ShotsHit,DamageTotal,"
  305. "HitsWithKnownDistance,DistanceTotal) "
  306. "values (\"%s\",\"%s\",%s,%d,%d,%d,%d,%d,%d,%llu)",
  307. szStatsFileUserID, mapname.c_str(), szDate, i, weaponStats.iShotsFired, weaponStats.iCritShotsFired, weaponStats.iHits, weaponStats.iTotalDamage,
  308. weaponStats.iHitsWithKnownDistance, weaponStats.iTotalDistance );
  309. // Msg( "%s\n", q );
  310. int retcode = sql->Execute( q );
  311. if ( retcode != 0 )
  312. {
  313. Msg( "Query %s failed\n", q );
  314. return;
  315. }
  316. }
  317. }
  318. }
  319. bool g_bDeathOnly = false;
  320. //-----------------------------------------------------------------------------
  321. // Purpose:
  322. //-----------------------------------------------------------------------------
  323. int TF_ParseCustomGameStatsData( ParseContext_t *ctx )
  324. {
  325. FILE *fp = fopen( ctx->file, "rb" );
  326. if ( !fp )
  327. return CUSTOMDATA_FAILED;
  328. CUtlBuffer statsBuffer;
  329. struct _stat sb;
  330. _stat( ctx->file, &sb );
  331. statsBuffer.Clear();
  332. statsBuffer.EnsureCapacity( sb.st_size );
  333. fread( statsBuffer.Base(), sb.st_size, 1, fp );
  334. statsBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, sb.st_size );
  335. fclose( fp );
  336. if ( memcmp( statsBuffer.Base(), "\"gamestats\"", 11 ) == 0 )
  337. {
  338. Msg( "Got new-style file format that we don't support, skipping\n" );
  339. return CUSTOMDATA_SUCCESS;
  340. }
  341. if ( memcmp( statsBuffer.Base(), "PERFDATA:", 9 ) == 0 )
  342. {
  343. ProcessPerfData( ctx->mysql, sb.st_mtime, "tf_perfdata", ( ( char * ) statsBuffer.Base() ) + 9 );
  344. return CUSTOMDATA_SUCCESS;
  345. }
  346. char shortname[ 128 ];
  347. Q_FileBase( ctx->file, shortname, sizeof( shortname ) );
  348. char szCurrentStatsFileUserID[17];
  349. int iCurrentStatsFileVersion;
  350. iCurrentStatsFileVersion = statsBuffer.GetShort();
  351. statsBuffer.Get( szCurrentStatsFileUserID, 16 );
  352. szCurrentStatsFileUserID[ sizeof( szCurrentStatsFileUserID ) - 1 ] = 0;
  353. unsigned int iCheckIfStandardDataSaved = statsBuffer.GetUnsignedInt();
  354. if( iCheckIfStandardDataSaved != GAMESTATS_STANDARD_NOT_SAVED )
  355. {
  356. // we don't care about the standard data, so why is it here?
  357. return CUSTOMDATA_FAILED;
  358. }
  359. // check for custom data
  360. bool bHasCustomData = (statsBuffer.TellPut() != statsBuffer.TellGet());
  361. if( !bHasCustomData )
  362. {
  363. // where's our data?
  364. return CUSTOMDATA_FAILED;
  365. }
  366. if ( !LoadCustomDataFromBuffer( statsBuffer ) )
  367. return CUSTOMDATA_FAILED;
  368. if( ctx->describeonly )
  369. {
  370. DescribeTF2Stats();
  371. }
  372. else
  373. {
  374. if ( CommandLine()->CheckParm( "-deathsonly" ) )
  375. {
  376. g_bDeathOnly = true;
  377. }
  378. InsertTF2Data( g_bDeathOnly, szCurrentStatsFileUserID, sb.st_mtime, ctx->mysql, iCurrentStatsFileVersion );
  379. }
  380. return CUSTOMDATA_SUCCESS;
  381. }
  382. //-----------------------------------------------------------------------------
  383. // Purpose: Called after all new files have been parsed & imported
  384. //-----------------------------------------------------------------------------
  385. void TF_PostImport( IMySQL *sql )
  386. {
  387. #if 0 // now handled by PHP script
  388. if ( g_bDeathOnly )
  389. return;
  390. // All the new data files have been imported to SQL. Now, do a rollup of the raw data into the rollup tables.
  391. // delete existing rollup for class data
  392. int retcode = sql->Execute( "delete from tf_classdata_rollup" );
  393. if ( 0 != retcode )
  394. {
  395. Msg( "Failed to delete from tf_classdata_rollup\n" );
  396. return;
  397. }
  398. // create new rollup for class data
  399. retcode = sql->Execute( "insert into tf_classdata_rollup (class,spawns,totaltime,score,kills,deaths,assists,captures) "
  400. "select class,sum(spawns),sum(totaltime),sum(score),sum(kills),sum(deaths),sum(assists),sum(captures) from tf_classdata group by class;" );
  401. if ( 0 != retcode )
  402. {
  403. Msg( "Failed to create class data rollup\n" );
  404. return;
  405. }
  406. // delete existing rollup for map data
  407. retcode = sql->Execute( "delete from tf_mapdata_rollup" );
  408. if ( 0 != retcode )
  409. {
  410. Msg( "Failed to delete from tf_mapdata_rollup\n" );
  411. return;
  412. }
  413. // create new rollup for map data
  414. retcode = sql->Execute( "insert into tf_mapdata_rollup (mapname,roundsplayed,totaltime,bluewins,redwins,stalemates) "
  415. "select mapname,sum(roundsplayed),sum(totaltime),sum(bluewins),sum(redwins),sum(stalemates) from tf_mapdata group by mapname;" );
  416. if ( 0 != retcode )
  417. {
  418. Msg( "Failed to create map data rollup\n" );
  419. return;
  420. }
  421. #endif
  422. }