Counter Strike : Global Offensive Source Code
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.

451 lines
18 KiB

  1. //====== Copyright � 1996-2005, Valve Corporation, All rights reserved. =======
  2. //
  3. // Purpose: Uploads KeyValue stats to the new SteamWorks gamestats system.
  4. //
  5. //=============================================================================
  6. #include "cbase.h"
  7. #include "cdll_int.h"
  8. #include "tier2/tier2.h"
  9. #include <time.h>
  10. #include "c_playerresource.h"
  11. #include "steam/isteamutils.h"
  12. #include "steamworks_gamestats_client.h"
  13. #include "icommandline.h"
  14. // NOTE: This has to be the last file included!
  15. #include "tier0/memdbgon.h"
  16. ConVar steamworks_sessionid_client( "steamworks_sessionid_client", "0", FCVAR_HIDDEN, "The client session ID for the new steamworks gamestats." );
  17. extern ConVar steamworks_sessionid_server;
  18. static CSteamWorksGameStatsClient g_SteamWorksGameStatsClient;
  19. extern ConVar developer;
  20. void Show_Steam_Stats_Session_ID( void )
  21. {
  22. DevMsg( "Client session ID (%s).\n", steamworks_sessionid_client.GetString() );
  23. DevMsg( "Server session ID (%s).\n", steamworks_sessionid_server.GetString() );
  24. }
  25. static ConCommand ShowSteamStatsSessionID( "ShowSteamStatsSessionID", Show_Steam_Stats_Session_ID, "Prints out the game stats session ID's (developer convar must be set to non-zero).", FCVAR_DEVELOPMENTONLY );
  26. //-----------------------------------------------------------------------------
  27. // Purpose: Clients store the server's session IDs so we can associate client rows with server rows.
  28. //-----------------------------------------------------------------------------
  29. void ServerSessionIDChangeCallback( IConVar *pConVar, const char *pOldString, float flOldValue )
  30. {
  31. ConVarRef var( pConVar );
  32. if ( var.IsValid() )
  33. {
  34. // Treat the variable as a string, since the sessionID is 64 bit and the convar int interface is only 32 bit.
  35. const char* pVarString = var.GetString();
  36. uint64 newServerSessionID = Q_atoi64( pVarString );
  37. GetSteamWorksGameStatsClient().SetServerSessionID( newServerSessionID );
  38. }
  39. }
  40. //-----------------------------------------------------------------------------
  41. // Purpose: Returns a reference to the global object
  42. //-----------------------------------------------------------------------------
  43. CSteamWorksGameStatsClient& GetSteamWorksGameStatsClient()
  44. {
  45. return g_SteamWorksGameStatsClient;
  46. }
  47. //-----------------------------------------------------------------------------
  48. // Purpose: Constructor. Sets up the steam callbacks accordingly depending on client/server dll
  49. //-----------------------------------------------------------------------------
  50. CSteamWorksGameStatsClient::CSteamWorksGameStatsClient() : BaseClass( "CSteamWorksGameStatsClient", "steamworks_sessionid_client" )
  51. {
  52. Reset();
  53. }
  54. //-----------------------------------------------------------------------------
  55. // Purpose: Reset uploader state.
  56. //-----------------------------------------------------------------------------
  57. void CSteamWorksGameStatsClient::Reset()
  58. {
  59. BaseClass::Reset();
  60. m_HumanCntInGame = 0;
  61. m_FriendCntInGame = 0;
  62. memset( m_pzPlayerName, 0, ARRAYSIZE(m_pzPlayerName) );
  63. steamworks_sessionid_client.SetValue( 0 );
  64. ClearServerSessionID();
  65. }
  66. //-----------------------------------------------------------------------------
  67. // Purpose: Init function from CAutoGameSystemPerFrame and must return true.
  68. //-----------------------------------------------------------------------------
  69. bool CSteamWorksGameStatsClient::Init()
  70. {
  71. BaseClass::Init();
  72. // TODO: This event doesn't exist in dota. Hook it up?
  73. //ListenForGameEvent( "client_disconnect" );
  74. steamworks_sessionid_server.InstallChangeCallback( ServerSessionIDChangeCallback );
  75. return true;
  76. }
  77. void CSteamWorksGameStatsClient::AddSessionIDsToTable( int iTableID )
  78. {
  79. // Our client side session.
  80. WriteInt64ToTable( m_SessionID, iTableID, "SessionID" );
  81. // The session of the server we are connected to.
  82. WriteInt64ToTable( m_ServerSessionID, iTableID, "ServerSessionID" );
  83. }
  84. //-----------------------------------------------------------------------------
  85. // Purpose: Event handler for gathering basic info as well as ending sessions.
  86. //-----------------------------------------------------------------------------
  87. void CSteamWorksGameStatsClient::FireGameEvent( IGameEvent *event )
  88. {
  89. if ( !event )
  90. return;
  91. const char *pEventName = event->GetName();
  92. if ( FStrEq( "client_disconnect", pEventName ) )
  93. {
  94. ClientDisconnect();
  95. }
  96. else if ( FStrEq( "player_changename", pEventName ) )
  97. {
  98. V_strncpy( m_pzPlayerName, event->GetString( "newname", "No Player Name" ), sizeof( m_pzPlayerName ) );
  99. }
  100. else
  101. {
  102. BaseClass::FireGameEvent( event );
  103. }
  104. }
  105. //-----------------------------------------------------------------------------
  106. // Purpose: Sets the server session ID but ONLY if it's not 0. We are using this to avoid a race
  107. // condition where a server sends their session stats before a client does, thereby,
  108. // resetting the client's server session ID to 0.
  109. //-----------------------------------------------------------------------------
  110. void CSteamWorksGameStatsClient::SetServerSessionID( uint64 serverSessionID )
  111. {
  112. if ( !serverSessionID )
  113. return;
  114. if ( serverSessionID != m_ActiveSession.m_ServerSessionID )
  115. {
  116. m_ActiveSession.m_ServerSessionID = serverSessionID;
  117. m_ActiveSession.m_ConnectTime = GetTimeSinceEpoch();
  118. m_ActiveSession.m_DisconnectTime = 0;
  119. m_iServerConnectCount++;
  120. }
  121. m_ServerSessionID = serverSessionID;
  122. }
  123. //-----------------------------------------------------------------------------
  124. // Purpose: Writes the disconnect time to the current server session entry.
  125. //-----------------------------------------------------------------------------
  126. void CSteamWorksGameStatsClient::ClientDisconnect()
  127. {
  128. Assert( 0 ); // i want to remove this.
  129. if ( m_ActiveSession.m_ServerSessionID == 0 )
  130. return;
  131. m_SteamWorksInterface = GetInterface();
  132. if ( !m_SteamWorksInterface )
  133. return;
  134. if ( !IsCollectingAnyData() )
  135. return;
  136. uint64 ulRowID = 0;
  137. m_SteamWorksInterface->AddNewRow( &ulRowID, m_SessionID, "ClientSessionLookup" );
  138. WriteInt64ToTable( m_SessionID, ulRowID, "SessionID" );
  139. WriteInt64ToTable( m_ActiveSession.m_ServerSessionID, ulRowID, "ServerSessionID" );
  140. WriteIntToTable( m_ActiveSession.m_ConnectTime, ulRowID, "ConnectTime" );
  141. WriteIntToTable( GetTimeSinceEpoch(), ulRowID, "DisconnectTime" );
  142. m_SteamWorksInterface->CommitRow( ulRowID );
  143. m_ActiveSession.Reset();
  144. }
  145. //-----------------------------------------------------------------------------
  146. // Purpose: Uploads any end of session rows.
  147. //-----------------------------------------------------------------------------
  148. void CSteamWorksGameStatsClient::WriteSessionRow()
  149. {
  150. m_SteamWorksInterface = GetInterface();
  151. if ( !m_SteamWorksInterface )
  152. return;
  153. m_SteamWorksInterface->AddSessionAttributeInt64( m_SessionID, "ServerSessionID", m_ServerSessionID );
  154. m_SteamWorksInterface->AddSessionAttributeString( m_SessionID, "ServerIP", m_pzServerIP );
  155. m_SteamWorksInterface->AddSessionAttributeString( m_SessionID, "ServerName", m_pzHostName );
  156. m_SteamWorksInterface->AddSessionAttributeString( m_SessionID, "StartMap", m_pzMapStart );
  157. m_SteamWorksInterface->AddSessionAttributeString( m_SessionID, "PlayerName", m_pzPlayerName );
  158. m_SteamWorksInterface->AddSessionAttributeInt( m_SessionID, "PlayersInGame", m_HumanCntInGame );
  159. m_SteamWorksInterface->AddSessionAttributeInt( m_SessionID, "FriendsInGame", m_FriendCntInGame );
  160. BaseClass::WriteSessionRow();
  161. }
  162. void CSteamWorksGameStatsClient::OnSteamSessionIssued( GameStatsSessionIssued_t *pResult, bool bError )
  163. {
  164. BaseClass::OnSteamSessionIssued( pResult, bError );
  165. m_FriendCntInGame = GetFriendCountInGame();
  166. CBasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
  167. const char *pPlayerName = pPlayer ? pPlayer->GetPlayerName() : "unknown";
  168. V_strncpy( m_pzPlayerName, pPlayerName, sizeof( m_pzPlayerName ) );
  169. }
  170. //-----------------------------------------------------------------------------
  171. // Purpose: Reports client's perf data at the end of a client session.
  172. //-----------------------------------------------------------------------------
  173. void CSteamWorksGameStatsClient::AddClientPerfData( KeyValues *pKV )
  174. {
  175. m_SteamWorksInterface = GetInterface();
  176. if ( !m_SteamWorksInterface )
  177. return;
  178. if ( !IsCollectingAnyData() )
  179. return;
  180. RTime32 currentTime = GetTimeSinceEpoch();
  181. uint64 uSessionID = m_SessionID;
  182. uint64 ulRowID = 0;
  183. m_SteamWorksInterface->AddNewRow( &ulRowID, uSessionID, "CSGOClientPerfData" );
  184. if ( !ulRowID )
  185. return;
  186. WriteInt64ToTable( m_SessionID, ulRowID, "SessionID" );
  187. // WriteInt64ToTable( m_ServerSessionID, ulRowID, "ServerSessionID" );
  188. WriteIntToTable( currentTime, ulRowID, "TimeSubmitted" );
  189. //WriteStringToTable( pKV->GetString( "Map/mapname" ), ulRowID, "MapID");
  190. WriteIntToTable( pKV->GetInt( "appid" ), ulRowID, "AppID");
  191. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgFPS" ), ulRowID, "AvgFPS");
  192. WriteFloatToTable( pKV->GetFloat( "map/perfdata/MinFPS" ), ulRowID, "MinFPS");
  193. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/MaxFPS" ), ulRowID, "MaxFPS");
  194. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/StdDevFPS" ), ulRowID, "StdDevFPS");
  195. WriteInt64ToTable( pKV->GetUint64( "Map/perfdata/FrameHistAll" ), ulRowID, "FrameHistAll" );
  196. WriteInt64ToTable( pKV->GetUint64( "Map/perfdata/FrameHistGame1" ), ulRowID, "FrameHistGame1" );
  197. WriteInt64ToTable( pKV->GetUint64( "Map/perfdata/FrameHistGame2" ), ulRowID, "FrameHistGame2" );
  198. WriteInt64ToTable( pKV->GetUint64( "Map/perfdata/FrameHistGame3" ), ulRowID, "FrameHistGame3" );
  199. WriteInt64ToTable( pKV->GetUint64( "Map/perfdata/FrameHistGame4" ), ulRowID, "FrameHistGame4" );
  200. WriteStringToTable( pKV->GetString( "CPUID" ), ulRowID, "CPUID" );
  201. WriteFloatToTable( pKV->GetFloat( "CPUGhz" ), ulRowID, "CPUGhz");
  202. WriteInt64ToTable( pKV->GetUint64( "CPUModel" ), ulRowID, "CPUModel" );
  203. WriteInt64ToTable( pKV->GetUint64( "CPUFeatures0" ), ulRowID, "CPUFeatures0" );
  204. WriteInt64ToTable( pKV->GetUint64( "CPUFeatures1" ), ulRowID, "CPUFeatures1" );
  205. WriteInt64ToTable( pKV->GetUint64( "CPUFeatures2" ), ulRowID, "CPUFeatures2" );
  206. WriteIntToTable( pKV->GetInt( "NumCores" ), ulRowID, "NumCores" );
  207. WriteStringToTable( pKV->GetString( "GPUDrv" ), ulRowID, "GPUDrv");
  208. WriteIntToTable( pKV->GetInt( "GPUVendor" ), ulRowID, "GPUVendor");
  209. WriteIntToTable( pKV->GetInt( "GPUDeviceID" ), ulRowID, "GPUDeviceID");
  210. WriteIntToTable( pKV->GetInt( "GPUDriverVersion" ), ulRowID, "GPUDriverVersion");
  211. WriteIntToTable( pKV->GetInt( "DxLvl" ), ulRowID, "DxLvl");
  212. WriteIntToTable( pKV->GetInt( "IsSplitScreen" ), ulRowID, "IsSplitScreen");
  213. WriteIntToTable( pKV->GetBool( "Map/Windowed" ), ulRowID, "Windowed");
  214. WriteIntToTable( pKV->GetBool( "Map/WindowedNoBorder" ), ulRowID, "WindowedNoBorder");
  215. WriteIntToTable( pKV->GetInt( "width" ), ulRowID, "Width");
  216. WriteIntToTable( pKV->GetInt( "height" ), ulRowID, "Height");
  217. WriteIntToTable( pKV->GetInt( "Map/UsedVoice" ), ulRowID, "Usedvoiced");
  218. WriteStringToTable( pKV->GetString( "Map/Language" ), ulRowID, "Language");
  219. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgServerPing" ), ulRowID, "AvgServerPing");
  220. WriteIntToTable( pKV->GetInt( "Map/Caption" ), ulRowID, "IsCaptioned");
  221. WriteIntToTable( pKV->GetInt( "IsPC" ), ulRowID, "IsPC");
  222. WriteIntToTable( pKV->GetInt( "Map/Cheats" ), ulRowID, "Cheats");
  223. WriteIntToTable( pKV->GetInt( "Map/MapTime" ), ulRowID, "MapTime");
  224. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgMainThreadTime" ), ulRowID, "AvgMainThreadTime");
  225. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/StdDevMainThreadTime" ), ulRowID, "StdMainThreadTime");
  226. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgMainThreadWaitTime" ), ulRowID, "AvgMainThreadWaitTime" );
  227. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/StdDevMainThreadWaitTime" ), ulRowID, "StdMainThreadWaitTime" );
  228. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgRenderThreadTime" ), ulRowID, "AvgRenderThreadTime" );
  229. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/StdDevRenderThreadTime" ), ulRowID, "StdRenderThreadTime" );
  230. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/AvgRenderThreadWaitTime" ), ulRowID, "AvgRenderThreadWaitTime" );
  231. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/StdDevRenderThreadWaitTime" ), ulRowID, "StdRenderThreadWaitTime" );
  232. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/PercentageCPUThrottledToLevel1" ), ulRowID, "PercentageCPUThrottledToLevel1");
  233. WriteFloatToTable( pKV->GetFloat( "Map/perfdata/PercentageCPUThrottledToLevel2" ), ulRowID, "PercentageCPUThrottledToLevel2");
  234. m_SteamWorksInterface->CommitRow( ulRowID );
  235. }
  236. //-----------------------------------------------------------------------------
  237. // Purpose: Reports client's game event stats.
  238. //-----------------------------------------------------------------------------
  239. bool CSteamWorksGameStatsClient::AddCsgoGameEventStat( char const *szMapName, char const *szEvent, Vector const &pos, QAngle const &ang, uint64 ullData, int16 nRound, int16 numRoundSecondsElapsed )
  240. {
  241. m_SteamWorksInterface = GetInterface();
  242. if ( !m_SteamWorksInterface )
  243. return false;
  244. if ( !IsCollectingAnyData() )
  245. return false;
  246. // UNUSED: RTime32 currentTime = GetTimeSinceEpoch();
  247. uint64 uSessionID = m_SessionID;
  248. uint64 ulRowID = 0;
  249. m_SteamWorksInterface->AddNewRow( &ulRowID, uSessionID, "CSGOGameEvent" );
  250. if ( !ulRowID )
  251. return false;
  252. WriteInt64ToTable( m_SessionID, ulRowID, "SessionID" );
  253. static int s_nCounter = 0;
  254. WriteIntToTable( ++ s_nCounter, ulRowID, "EventCount" );
  255. WriteStringToTable( szEvent, ulRowID, "EventID" );
  256. WriteStringToTable( szMapName, ulRowID, "MapID" );
  257. WriteIntToTable( (int16) Clamp<float>( pos[0], -MAX_COORD_FLOAT, MAX_COORD_FLOAT ), ulRowID, "PosX" );
  258. WriteIntToTable( (int16) Clamp<float>( pos[1], -MAX_COORD_FLOAT, MAX_COORD_FLOAT ), ulRowID, "PosY" );
  259. WriteIntToTable( (int16) Clamp<float>( pos[2], -MAX_COORD_FLOAT, MAX_COORD_FLOAT ), ulRowID, "PosZ" );
  260. WriteFloatToTable( ang[0], ulRowID, "ViewX" );
  261. WriteFloatToTable( ang[1], ulRowID, "ViewY" );
  262. WriteFloatToTable( ang[2], ulRowID, "ViewZ" );
  263. WriteIntToTable( nRound, ulRowID, "GameRound" );
  264. WriteIntToTable( numRoundSecondsElapsed, ulRowID, "GameTime" );
  265. WriteInt64ToTable( ullData, ulRowID, "Data" );
  266. m_SteamWorksInterface->CommitRow( ulRowID );
  267. return true;
  268. }
  269. //-----------------------------------------------------------------------------
  270. // Purpose: Reports client's VPK load stats.
  271. //-----------------------------------------------------------------------------
  272. void CSteamWorksGameStatsClient::AddVPKLoadStats( KeyValues *pKV )
  273. {
  274. m_SteamWorksInterface = GetInterface();
  275. if ( !m_SteamWorksInterface )
  276. return;
  277. if ( !IsCollectingAnyData() )
  278. return;
  279. RTime32 currentTime = GetTimeSinceEpoch();
  280. uint64 uSessionID = m_SessionID;
  281. uint64 ulRowID = 0;
  282. m_SteamWorksInterface->AddNewRow( &ulRowID, uSessionID, "CSGOClientVPKFileStats" );
  283. if ( !ulRowID )
  284. return;
  285. WriteInt64ToTable( m_SessionID, ulRowID, "SessionID" );
  286. WriteIntToTable( currentTime, ulRowID, "TimeSubmitted" );
  287. WriteIntToTable( pKV->GetInt( "BytesReadFromCache" ), ulRowID, "BytesReadFromCache");
  288. WriteIntToTable( pKV->GetInt( "ItemsReadFromCache" ), ulRowID, "ItemsReadFromCache");
  289. WriteIntToTable( pKV->GetInt( "DiscardsFromCache" ), ulRowID, "DiscardsFromCache");
  290. WriteIntToTable( pKV->GetInt( "AddedToCache" ), ulRowID, "AddedToCache");
  291. WriteIntToTable( pKV->GetInt( "CacheMisses" ), ulRowID, "CacheMisses");
  292. WriteIntToTable( pKV->GetInt( "FileErrorCount" ), ulRowID, "FileErrorCount");
  293. WriteIntToTable( pKV->GetInt( "FileErrorsCorrected" ), ulRowID, "FileErrorsCorrected");
  294. WriteIntToTable( pKV->GetInt( "FileResultsDifferent" ), ulRowID, "FileResultsDifferent");
  295. m_SteamWorksInterface->CommitRow( ulRowID );
  296. }
  297. //-----------------------------------------------------------------------------
  298. // Purpose: Reports any VPK file load errors
  299. //-----------------------------------------------------------------------------
  300. void CSteamWorksGameStatsClient::AddVPKFileLoadErrorData( KeyValues *pKV )
  301. {
  302. m_SteamWorksInterface = GetInterface();
  303. if ( !m_SteamWorksInterface )
  304. return;
  305. if ( !IsCollectingAnyData() )
  306. return;
  307. RTime32 currentTime = GetTimeSinceEpoch();
  308. uint64 uSessionID = m_SessionID;
  309. uint64 ulRowID = 0;
  310. for ( KeyValues *pkvSubKey = pKV->GetFirstTrueSubKey(); pkvSubKey != NULL; pkvSubKey = pkvSubKey->GetNextTrueSubKey() )
  311. {
  312. m_SteamWorksInterface->AddNewRow( &ulRowID, uSessionID, "CSGOClientVPKFileError" );
  313. if ( !ulRowID )
  314. return;
  315. WriteInt64ToTable( m_SessionID, ulRowID, "SessionID" );
  316. WriteIntToTable( currentTime, ulRowID, "TimeSubmitted" );
  317. WriteIntToTable( pkvSubKey->GetInt( "PackFileID" ), ulRowID, "PackFileID");
  318. WriteIntToTable( pkvSubKey->GetInt( "PackFileNumber" ), ulRowID, "PackFileNumber");
  319. WriteIntToTable( pkvSubKey->GetInt( "FileFraction" ), ulRowID, "FileFraction");
  320. WriteStringToTable( pkvSubKey->GetString( "ChunkMd5Master" ), ulRowID, "ChunkMd5MasterText");
  321. WriteStringToTable( pkvSubKey->GetString( "ChunkMd5First" ), ulRowID, "ChunkMd5FirstText");
  322. WriteStringToTable( pkvSubKey->GetString( "ChunkMd5Second" ), ulRowID, "ChunkMd5SecondText");
  323. m_SteamWorksInterface->CommitRow( ulRowID );
  324. }
  325. }
  326. //-------------------------------------------------------------------------------------------------
  327. /**
  328. * Purpose: Calculates the number of friends in the game
  329. */
  330. int CSteamWorksGameStatsClient::GetFriendCountInGame()
  331. {
  332. // Get the number of steam friends in game
  333. int friendsInOurGame = 0;
  334. #if !defined( NO_STEAM )
  335. // Do we have access to the steam API?
  336. if ( AccessToSteamAPI() )
  337. {
  338. CSteamID m_SteamID = steamapicontext->SteamUser()->GetSteamID();
  339. // Let's get our game info so we can use that to test if our friends are connected to the same game as us
  340. FriendGameInfo_t myGameInfo;
  341. steamapicontext->SteamFriends()->GetFriendGamePlayed( m_SteamID, &myGameInfo );
  342. CSteamID myLobby = steamapicontext->SteamMatchmaking()->GetLobbyOwner( myGameInfo.m_steamIDLobby );
  343. // This returns the number of friends that are playing a game
  344. int activeFriendCnt = steamapicontext->SteamFriends()->GetFriendCount( k_EFriendFlagImmediate );
  345. // Check each active friend's lobby ID to see if they are in our game
  346. for ( int h=0; h< activeFriendCnt ; ++h )
  347. {
  348. FriendGameInfo_t friendInfo;
  349. CSteamID friendID = steamapicontext->SteamFriends()->GetFriendByIndex( h, k_EFriendFlagImmediate );
  350. if ( steamapicontext->SteamFriends()->GetFriendGamePlayed( friendID, &friendInfo ) )
  351. {
  352. // Does our friend have a valid lobby ID?
  353. if ( friendInfo.m_gameID.IsValid() )
  354. {
  355. // Get our friend's lobby info
  356. CSteamID friendLobby = steamapicontext->SteamMatchmaking()->GetLobbyOwner( friendInfo.m_steamIDLobby );
  357. // Double check the validity of the friend lobby ID then check to see if they are in our game
  358. if ( friendLobby.IsValid() && myLobby == friendLobby )
  359. {
  360. ++friendsInOurGame;
  361. }
  362. }
  363. }
  364. }
  365. }
  366. #endif // !NO_STEAM
  367. return friendsInOurGame;
  368. }