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.

386 lines
15 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================//
  7. #include "cbase.h"
  8. #include "tf_steamstats.h"
  9. #include "tf_hud_statpanel.h"
  10. #include "achievementmgr.h"
  11. #include "engine/imatchmaking.h"
  12. #include "ipresence.h"
  13. #include "../game/shared/tf/tf_shareddefs.h"
  14. #include "../game/shared/tf/tf_gamestats_shared.h"
  15. struct StatMap_t
  16. {
  17. const char *pszName;
  18. int iStat;
  19. int iLiveStat;
  20. };
  21. // subset of stats which we store in Steam
  22. StatMap_t g_SteamStats[] = {
  23. { "iNumberOfKills", TFSTAT_KILLS, PROPERTY_KILLS, },
  24. { "iDamageDealt", TFSTAT_DAMAGE, PROPERTY_DAMAGE_DEALT, },
  25. { "iPlayTime", TFSTAT_PLAYTIME, PROPERTY_PLAY_TIME, },
  26. { "iPointCaptures", TFSTAT_CAPTURES, PROPERTY_POINT_CAPTURES, },
  27. { "iPointDefenses", TFSTAT_DEFENSES, PROPERTY_POINT_DEFENSES, },
  28. { "iDominations", TFSTAT_DOMINATIONS, PROPERTY_DOMINATIONS, },
  29. { "iRevenge", TFSTAT_REVENGE, PROPERTY_REVENGE, },
  30. { "iPointsScored", TFSTAT_POINTSSCORED, PROPERTY_POINTS_SCORED, },
  31. { "iBuildingsDestroyed", TFSTAT_BUILDINGSDESTROYED, PROPERTY_BUILDINGS_DESTROYED, },
  32. { "iNumInvulnerable", TFSTAT_INVULNS, PROPERTY_INVULNS, },
  33. { "iKillAssists", TFSTAT_KILLASSISTS, PROPERTY_KILL_ASSISTS, },
  34. };
  35. // class specific stats
  36. StatMap_t g_SteamStats_Pyro[] = {
  37. { "iFireDamage", TFSTAT_FIREDAMAGE, -1, }, // Added post-XBox, isn't saved in Live
  38. { NULL, 0, 0, },
  39. };
  40. StatMap_t g_SteamStats_Demoman[] = {
  41. { "iBlastDamage", TFSTAT_BLASTDAMAGE, -1, }, // Added post-XBox, isn't saved in Live
  42. { NULL, 0, 0, },
  43. };
  44. StatMap_t g_SteamStats_Engineer[] = {
  45. { "iBuildingsBuilt", TFSTAT_BUILDINGSBUILT, PROPERTY_BUILDINGS_BUILT, },
  46. { "iSentryKills", TFSTAT_MAXSENTRYKILLS, PROPERTY_SENTRY_KILLS, },
  47. { "iNumTeleports", TFSTAT_TELEPORTS, PROPERTY_TELEPORTS, },
  48. { NULL, 0, 0, },
  49. };
  50. StatMap_t g_SteamStats_Medic[] = {
  51. { "iHealthPointsHealed", TFSTAT_HEALING, PROPERTY_HEALTH_POINTS_HEALED, },
  52. { NULL, 0, 0, },
  53. };
  54. StatMap_t g_SteamStats_Sniper[] = {
  55. { "iHeadshots", TFSTAT_HEADSHOTS, PROPERTY_HEADSHOTS, },
  56. { NULL, 0, 0, },
  57. };
  58. StatMap_t g_SteamStats_Spy[] = {
  59. { "iHeadshots", TFSTAT_HEADSHOTS, PROPERTY_HEADSHOTS, },
  60. { "iBackstabs", TFSTAT_BACKSTABS, PROPERTY_BACKSTABS, },
  61. { "iHealthPointsLeached", TFSTAT_HEALTHLEACHED, PROPERTY_HEALTH_POINTS_LEACHED, },
  62. { NULL, 0, 0, },
  63. };
  64. StatMap_t* g_SteamStats_Class[] = {
  65. NULL, // Undefined
  66. NULL, // Scout
  67. g_SteamStats_Sniper, // Sniper
  68. NULL, // Soldier
  69. g_SteamStats_Demoman, // Demoman
  70. g_SteamStats_Medic, // Medic
  71. NULL, // Heavy
  72. g_SteamStats_Pyro, // Pyro
  73. g_SteamStats_Spy, // Spy
  74. g_SteamStats_Engineer, // Engineer
  75. };
  76. // subset of map stats which we store in Steam
  77. StatMap_t g_SteamMapStats[] = {
  78. { "iPlayTime", TFMAPSTAT_PLAYTIME, PROPERTY_PLAY_TIME, },
  79. };
  80. //-----------------------------------------------------------------------------
  81. // Purpose: Constructor
  82. //-----------------------------------------------------------------------------
  83. CTFSteamStats::CTFSteamStats()
  84. {
  85. m_flTimeNextForceUpload = 0;
  86. }
  87. //-----------------------------------------------------------------------------
  88. // Purpose: called at init time after all systems are init'd. We have to
  89. // do this in PostInit because the Steam app ID is not available earlier
  90. //-----------------------------------------------------------------------------
  91. void CTFSteamStats::PostInit()
  92. {
  93. SetNextForceUploadTime();
  94. ListenForGameEvent( "player_stats_updated" );
  95. ListenForGameEvent( "user_data_downloaded" );
  96. }
  97. //-----------------------------------------------------------------------------
  98. // Purpose: called at level shutdown
  99. //-----------------------------------------------------------------------------
  100. void CTFSteamStats::LevelShutdownPreEntity()
  101. {
  102. // upload user stats to Steam on every map change
  103. UploadStats();
  104. }
  105. //-----------------------------------------------------------------------------
  106. // Purpose: called when the stats have changed in-game
  107. //-----------------------------------------------------------------------------
  108. void CTFSteamStats::FireGameEvent( IGameEvent *event )
  109. {
  110. const char *pEventName = event->GetName();
  111. if ( 0 == Q_strcmp( pEventName, "player_stats_updated" ) )
  112. {
  113. bool bForceUpload = event->GetBool( "forceupload" );
  114. // if we haven't uploaded stats in a long time, upload them
  115. if ( ( gpGlobals->curtime >= m_flTimeNextForceUpload ) || bForceUpload )
  116. {
  117. UploadStats();
  118. }
  119. }
  120. else if ( 0 == Q_strcmp( pEventName, "user_data_downloaded" ) )
  121. {
  122. Assert( steamapicontext->SteamUserStats() );
  123. if ( !steamapicontext->SteamUserStats() )
  124. return;
  125. CTFStatPanel *pStatPanel = GET_HUDELEMENT( CTFStatPanel );
  126. Assert( pStatPanel );
  127. for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ )
  128. {
  129. // Grab generic stats:
  130. ClassStats_t &classStats = CTFStatPanel::GetClassStats( iClass );
  131. for ( int iStat = 0; iStat < ARRAYSIZE( g_SteamStats ); iStat++ )
  132. {
  133. char szStatName[256];
  134. int iData;
  135. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  136. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  137. {
  138. classStats.accumulated.m_iStat[g_SteamStats[iStat].iStat] = iData;
  139. }
  140. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.max.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  141. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  142. {
  143. classStats.max.m_iStat[g_SteamStats[iStat].iStat] = iData;
  144. }
  145. // MVM Stats
  146. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  147. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  148. {
  149. classStats.accumulatedMVM.m_iStat[g_SteamStats[iStat].iStat] = iData;
  150. }
  151. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.max.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  152. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  153. {
  154. classStats.maxMVM.m_iStat[g_SteamStats[iStat].iStat] = iData;
  155. }
  156. }
  157. // Grab class specific stats:
  158. StatMap_t* pClassStatMap = g_SteamStats_Class[iClass];
  159. if ( pClassStatMap )
  160. {
  161. int iStat = 0;
  162. do
  163. {
  164. char szStatName[256];
  165. int iData;
  166. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  167. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  168. {
  169. classStats.accumulated.m_iStat[pClassStatMap[iStat].iStat] = iData;
  170. }
  171. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.max.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  172. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  173. {
  174. classStats.max.m_iStat[pClassStatMap[iStat].iStat] = iData;
  175. }
  176. // MVM Stats
  177. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  178. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  179. {
  180. classStats.accumulatedMVM.m_iStat[pClassStatMap[iStat].iStat] = iData;
  181. }
  182. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.max.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  183. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  184. {
  185. classStats.maxMVM.m_iStat[pClassStatMap[iStat].iStat] = iData;
  186. }
  187. iStat++;
  188. }
  189. while ( pClassStatMap[iStat].pszName );
  190. }
  191. }
  192. for ( int i = 0; i < GetItemSchema()->GetMapCount(); i++ )
  193. {
  194. const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByIndex( i );
  195. // Grab generic stats:
  196. MapStats_t &mapStats = CTFStatPanel::GetMapStats( pMap->GetStatsIdentifier() );
  197. for ( int iStat = 0; iStat < ARRAYSIZE( g_SteamMapStats ); iStat++ )
  198. {
  199. char szStatName[256];
  200. int iData;
  201. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", pMap->pszMapName, g_SteamMapStats[iStat].pszName );
  202. if ( steamapicontext->SteamUserStats()->GetStat( szStatName, &iData ) )
  203. {
  204. mapStats.accumulated.m_iStat[g_SteamMapStats[iStat].iStat] = iData;
  205. }
  206. }
  207. }
  208. IGameEvent * pEvent = gameeventmanager->CreateEvent( "player_stats_updated" );
  209. if ( pEvent )
  210. {
  211. pEvent->SetBool( "forceupload", false );
  212. gameeventmanager->FireEventClientSide( pEvent );
  213. }
  214. pStatPanel->SetStatsChanged( true );
  215. pStatPanel->UpdateStatSummaryPanel();
  216. }
  217. }
  218. //-----------------------------------------------------------------------------
  219. // Purpose: Uploads stats for current Steam user to Steam
  220. //-----------------------------------------------------------------------------
  221. void CTFSteamStats::UploadStats()
  222. {
  223. if ( IsX360() )
  224. {
  225. ReportLiveStats();
  226. return;
  227. }
  228. // Only upload if Steam is running & the achievement manager exists.
  229. if ( !steamapicontext->SteamUserStats() )
  230. return;
  231. CAchievementMgr *pAchievementMgr = dynamic_cast<CAchievementMgr *>( engine->GetAchievementMgr() );
  232. if ( !pAchievementMgr )
  233. return;
  234. // Stomp local steam context stats with those in the stat panel.
  235. for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ )
  236. {
  237. // Set generic stats:
  238. ClassStats_t &classStats = CTFStatPanel::GetClassStats( iClass );
  239. for ( int iStat = 0; iStat < ARRAYSIZE( g_SteamStats ); iStat++ )
  240. {
  241. char szStatName[256];
  242. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  243. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.accumulated.m_iStat[g_SteamStats[iStat].iStat] );
  244. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.max.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  245. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.max.m_iStat[g_SteamStats[iStat].iStat] );
  246. // MVM Stats
  247. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  248. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.accumulatedMVM.m_iStat[g_SteamStats[iStat].iStat] );
  249. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.max.%s", g_aPlayerClassNames_NonLocalized[iClass], g_SteamStats[iStat].pszName );
  250. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.maxMVM.m_iStat[g_SteamStats[iStat].iStat] );
  251. }
  252. // Set class specific stats:
  253. StatMap_t* pClassStatMap = g_SteamStats_Class[iClass];
  254. if ( pClassStatMap )
  255. {
  256. int iStat = 0;
  257. do
  258. {
  259. char szStatName[256];
  260. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  261. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.accumulated.m_iStat[pClassStatMap[iStat].iStat] );
  262. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.max.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  263. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.max.m_iStat[pClassStatMap[iStat].iStat] );
  264. // MVM Stats
  265. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.accum.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  266. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.accumulatedMVM.m_iStat[pClassStatMap[iStat].iStat] );
  267. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.mvm.max.%s", g_aPlayerClassNames_NonLocalized[iClass], pClassStatMap[iStat].pszName );
  268. steamapicontext->SteamUserStats()->SetStat( szStatName, classStats.maxMVM.m_iStat[pClassStatMap[iStat].iStat] );
  269. iStat++;
  270. }
  271. while ( pClassStatMap[iStat].pszName );
  272. }
  273. }
  274. // Stomp local steam context stats with those in the stat panel.
  275. for ( int i = 0; i < GetItemSchema()->GetMapCount(); i++ )
  276. {
  277. const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByIndex( i );
  278. // Set generic stats:
  279. MapStats_t &mapStats = CTFStatPanel::GetMapStats( pMap->GetStatsIdentifier() );
  280. for ( int iStat = 0; iStat < ARRAYSIZE( g_SteamMapStats ); iStat++ )
  281. {
  282. char szStatName[256];
  283. Q_snprintf( szStatName, ARRAYSIZE( szStatName ), "%s.accum.%s", pMap->pszMapName, g_SteamMapStats[iStat].pszName );
  284. steamapicontext->SteamUserStats()->SetStat( szStatName, mapStats.accumulated.m_iStat[g_SteamMapStats[iStat].iStat] );
  285. }
  286. }
  287. // Send our local steam context stats to the server.
  288. pAchievementMgr->UploadUserData();
  289. SetNextForceUploadTime();
  290. // Now everything should be sync'd up (stat panel, local steam context, remote steam depot).
  291. }
  292. //-----------------------------------------------------------------------------
  293. // Purpose: Accumulate player stats and send them to matchmaking for reporting to Live
  294. //-----------------------------------------------------------------------------
  295. void CTFSteamStats::ReportLiveStats()
  296. {
  297. int statsTotals[ARRAYSIZE( g_SteamStats )];
  298. Q_memset( &statsTotals, 0, sizeof( statsTotals ) );
  299. for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass <= TF_LAST_NORMAL_CLASS; iClass++ )
  300. {
  301. ClassStats_t &classStats = CTFStatPanel::GetClassStats( iClass );
  302. for ( int iStat = 0; iStat < ARRAYSIZE( g_SteamStats ); iStat++ )
  303. {
  304. statsTotals[iStat] = MAX( statsTotals[iStat], classStats.max.m_iStat[g_SteamStats[iStat].iStat] );
  305. }
  306. }
  307. // send the stats to matchmaking
  308. for ( int i = 0; i < ARRAYSIZE( g_SteamStats ); ++i )
  309. {
  310. // Points scored is looked up by the stats reporting function
  311. if ( g_SteamStats[i].iLiveStat == PROPERTY_POINTS_SCORED )
  312. continue;
  313. // If we hit this assert, we've added a new stat that Live won't know how to store
  314. Assert( g_SteamStats[i].iLiveStat != -1 );
  315. if ( g_SteamStats[i].iLiveStat != -1 )
  316. {
  317. presence->SetStat( g_SteamStats[i].iLiveStat, statsTotals[i], XUSER_DATA_TYPE_INT32 );
  318. }
  319. }
  320. presence->UploadStats();
  321. }
  322. //-----------------------------------------------------------------------------
  323. // Purpose: sets the next time to force a stats upload at
  324. //-----------------------------------------------------------------------------
  325. void CTFSteamStats::SetNextForceUploadTime()
  326. {
  327. // pick a time a while from now (an hour +/- 15 mins) to upload stats if we haven't gotten a map change by then
  328. m_flTimeNextForceUpload = gpGlobals->curtime + ( 60 * RandomInt( 45, 75 ) );
  329. }
  330. CTFSteamStats g_TFSteamStats;