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.

807 lines
26 KiB

  1. //========= Copyright (C) 1996-2013, Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose: Helper for access to news
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================//
  7. #include "cbase.h"
  8. #include "gc_clientsystem.h"
  9. #include "filesystem.h"
  10. #include "workshop/ugc_utils.h"
  11. #include "econ/econ_controls.h"
  12. #include "ienginevgui.h"
  13. #include "vgui/ISystem.h"
  14. #include "vgui_controls/ImagePanel.h"
  15. #include "vgui_bitmapimage.h"
  16. #include "bitmap/bitmap.h"
  17. #include "imageutils.h"
  18. #include "tier2/fileutils.h"
  19. #include "checksum_sha1.h"
  20. //#include "matchmaking/imatchframework.h"
  21. //#include "engine/inetsupport.h"
  22. #include "rtime.h"
  23. #include "tf_streams.h"
  24. // memdbgon must be the last include file in a .cpp file!!!
  25. #include <tier0/memdbgon.h>
  26. // GC hello information
  27. //extern CMsgGCCStrike15_v2_MatchmakingGC2ClientHello g_GC2ClientHello;
  28. ConVar cl_streams_request_url( "cl_streams_request_url",
  29. "https://api.twitch.tv/kraken/streams?game=Team%20Fortress%202&limit=5"
  30. , FCVAR_DEVELOPMENTONLY, "Number of streams requested for display" );
  31. ConVar cl_streams_request_accept( "cl_streams_request_accept", "application/vnd.twitchtv.v3+json", FCVAR_DEVELOPMENTONLY, "Header for api request" );
  32. ConVar cl_streams_image_sfurl( "cl_streams_image_sfurl",
  33. "img://loadjpeg:(320x200):"
  34. , FCVAR_DEVELOPMENTONLY, "Format of Scaleform image representing the stream" );
  35. ConVar cl_streams_request_count( "cl_streams_request_count", "6", FCVAR_DEVELOPMENTONLY, "How many streams are displayed in main menu" );
  36. ConVar cl_streams_refresh_interval( "cl_streams_refresh_interval", "60", FCVAR_DEVELOPMENTONLY, "How often to refresh streams list" );
  37. ConVar cl_streams_write_response_file( "cl_streams_write_response_file", "", FCVAR_DEVELOPMENTONLY, "When set will save streams info file for diagnostics" );
  38. ConVar cl_streams_override_global_version( "cl_streams_override_global_version", "", FCVAR_DEVELOPMENTONLY, "When set will override global API version" );
  39. ConVar cl_streams_mytwitchtv_nolink( "cl_streams_mytwitchtv_nolink", "https://www.twitch.tv/settings/connections", FCVAR_DEVELOPMENTONLY, "Twitch.tv account linking URL" );
  40. ConVar cl_streams_mytwitchtv_channel( "cl_streams_mytwitchtv_channel", "https://www.twitch.tv/", FCVAR_DEVELOPMENTONLY, "Twitch.tv account channel URL" );
  41. static const char *s_pszCacheImagePath = "streams";
  42. //////////////////////////////////////////////////////////////////////////
  43. //
  44. // Files downloader
  45. //
  46. class CHelperStreamDownloadUrlToLocalFile;
  47. static CUtlVector< CHelperStreamDownloadUrlToLocalFile * > s_arrDeleteCHelperStreamDownloadUrlToLocalFile;
  48. class CHelperStreamDownloadUrlToLocalFile
  49. {
  50. public:
  51. CHelperStreamDownloadUrlToLocalFile( char const *szUrlGet, char const *szLocalFile, long lTimeStampLocal, CTFStreamPanel *pStreamPanel )
  52. {
  53. m_sUrlGet = szUrlGet;
  54. m_sLocalFile = szLocalFile;
  55. m_lTimestampLocal = lTimeStampLocal;
  56. m_hHTTPRequestHandle = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, m_sUrlGet.Get() );
  57. SteamAPICall_t hCall = NULL;
  58. if ( m_hHTTPRequestHandle && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandle, &hCall ) && hCall )
  59. {
  60. m_CallbackOnHTTPRequestCompleted.Set( hCall, this, &CHelperStreamDownloadUrlToLocalFile::Steam_OnHTTPRequestCompleted );
  61. }
  62. else
  63. {
  64. if ( m_hHTTPRequestHandle )
  65. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle );
  66. m_hHTTPRequestHandle = NULL;
  67. }
  68. m_hStreamPanel = pStreamPanel;
  69. }
  70. private:
  71. CUtlString m_sUrlGet;
  72. CUtlString m_sLocalFile;
  73. long m_lTimestampLocal;
  74. HTTPRequestHandle m_hHTTPRequestHandle;
  75. CCallResult< CHelperStreamDownloadUrlToLocalFile, HTTPRequestCompleted_t > m_CallbackOnHTTPRequestCompleted;
  76. DHANDLE<CTFStreamPanel> m_hStreamPanel;
  77. void Steam_OnHTTPRequestCompleted( HTTPRequestCompleted_t *p, bool bError )
  78. {
  79. if ( !m_hHTTPRequestHandle || ( p->m_hRequest != m_hHTTPRequestHandle ) )
  80. return;
  81. uint32 unBytes = 0;
  82. if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 )
  83. {
  84. DevMsg( "Request for '%s' succeeded (code: %u, size: %u)...\n", m_sUrlGet.Get(), p ? p->m_eStatusCode : 0, unBytes );
  85. CUtlBuffer bufFile;
  86. bufFile.EnsureCapacity( unBytes );
  87. if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) )
  88. {
  89. bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes );
  90. g_pFullFileSystem->WriteFile( m_sLocalFile.Get(), "GAME", bufFile );
  91. UGC_SetFileTime( m_sLocalFile.Get(), m_lTimestampLocal );
  92. }
  93. if ( m_hStreamPanel.Get() )
  94. {
  95. m_hStreamPanel.Get()->InvalidateLayout();
  96. }
  97. }
  98. else
  99. {
  100. DevMsg( "Request for '%s' failed '%s' (code: %u, size: %u)...\n", m_sUrlGet.Get(), bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes );
  101. }
  102. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest );
  103. m_hHTTPRequestHandle = NULL;
  104. s_arrDeleteCHelperStreamDownloadUrlToLocalFile.AddToTail( this );
  105. }
  106. };
  107. //////////////////////////////////////////////////////////////////////////
  108. //
  109. // Helper functions
  110. //
  111. static long Helper_ParseUpdatedTimestamp( char const *pchParseUpdatedAt )
  112. {
  113. // Twitch.tv format: "2013-03-04T05:27:27Z"
  114. return CRTime::RTime32FromRFC3339UTCString( pchParseUpdatedAt );
  115. }
  116. static void Helper_ConfigureStreamInfoPreviewImages( CStreamInfo &info, CTFStreamPanel *pStreamPanel )
  117. {
  118. // store global name for the info in the panel to ask for it when the image is ready to be displayed
  119. pStreamPanel->SetGlobalName( info.m_sGlobalName );
  120. char chPreviewImageLocal[ 2*MAX_PATH + 1 ] = {};
  121. char *pchLocalCur = chPreviewImageLocal, *pchLocalEnd = chPreviewImageLocal + Q_ARRAYSIZE( chPreviewImageLocal ) - 1;
  122. for ( char const *pch = info.m_sPreviewImage.Get(); *pch; ++ pch )
  123. {
  124. if ( pchLocalEnd - pchLocalCur < 4 )
  125. break;
  126. if ( ( ( pch[0] >= 'a' ) && ( pch[0] <= 'z' ) ) ||
  127. ( ( pch[0] >= 'A' ) && ( pch[0] <= 'Z' ) ) ||
  128. ( ( pch[0] >= '0' ) && ( pch[0] <= '9' ) ) ||
  129. ( pch[0] == '-' ) || ( pch[0] == '.' ) )
  130. {
  131. * ( pchLocalCur ++ ) = *pch;
  132. }
  133. else
  134. {
  135. * ( pchLocalCur ++ ) = '_';
  136. int iHigh = ( ( ( unsigned char )( *pch ) & 0xF0u ) >> 4 ) & 0xF;
  137. if ( iHigh >= 0 && iHigh <= 9 )
  138. * ( pchLocalCur ++ ) = '0' + iHigh;
  139. else
  140. * ( pchLocalCur ++ ) = 'A' + iHigh;
  141. int iLow = ( ( ( unsigned char )( *pch ) & 0xFu ) );
  142. if ( iLow >= 0 && iLow <= 9 )
  143. * ( pchLocalCur ++ ) = '0' + iLow;
  144. else if ( iLow >= 10 && iLow <= 15 )
  145. * ( pchLocalCur ++ ) = 'A' + iLow - 10;
  146. * ( pchLocalCur ++ ) = '_';
  147. }
  148. }
  149. if ( chPreviewImageLocal[0] )
  150. {
  151. // We might need to download the file, so make sure directory structure is valid
  152. static bool s_bCreateDirHierarchyDone = false;
  153. if ( !s_bCreateDirHierarchyDone )
  154. {
  155. s_bCreateDirHierarchyDone = true;
  156. // Cleanup old cached files
  157. if ( long lDirectoryTime = g_pFullFileSystem->GetFileTime( s_pszCacheImagePath, "GAME" ) )
  158. {
  159. FileFindHandle_t hFind = NULL;
  160. int numRemove = 0;
  161. for ( char const *szFileName = g_pFullFileSystem->FindFirst( CFmtStr( "%s/*", s_pszCacheImagePath ), &hFind );
  162. szFileName && *szFileName; szFileName = g_pFullFileSystem->FindNext( hFind ) )
  163. {
  164. if ( !Q_strcmp( ".", szFileName ) || !Q_strcmp( "..", szFileName ) ) continue;
  165. CFmtStr fmtFilename( "%s/%s", s_pszCacheImagePath, szFileName );
  166. long lFileTime = g_pFullFileSystem->GetFileTime( fmtFilename, "GAME" );
  167. if ( ( lFileTime >= lDirectoryTime - 72*3600 ) && ( lFileTime <= lDirectoryTime + 72*3600 ) )
  168. {
  169. // DevMsg( "Keeping file %s (%u : %u)\n", fmtFilename.Access(), lFileTime, lDirectoryTime );
  170. continue;
  171. }
  172. else
  173. {
  174. ++ numRemove;
  175. g_pFullFileSystem->RemoveFile( fmtFilename, "GAME" );
  176. }
  177. }
  178. DevMsg( 2, "Streams preview cache evicted %u files\n", numRemove );
  179. g_pFullFileSystem->FindClose( hFind );
  180. }
  181. g_pFullFileSystem->CreateDirHierarchy( s_pszCacheImagePath, "GAME" );
  182. }
  183. //
  184. // Work with the file cache
  185. //
  186. CFmtStr fmtLocalFile( "%s/%s", s_pszCacheImagePath, chPreviewImageLocal );
  187. info.m_sPreviewImageLocalFile = fmtLocalFile.Access();
  188. CFmtStr fmtPreviewSF( "%sstreams/%s", cl_streams_image_sfurl.GetString(), chPreviewImageLocal );
  189. info.m_sPreviewImageSF = fmtPreviewSF.Access();
  190. // See if we need to download that file
  191. long lFileTime = g_pFullFileSystem->GetFileTime( fmtLocalFile.Access(), "GAME" );
  192. // Parse "updated_at" attribute:
  193. long lUpdatedAtStamp = Helper_ParseUpdatedTimestamp( info.m_sUpdatedAtStamp.Get() );
  194. if ( lFileTime != lUpdatedAtStamp )
  195. {
  196. // Redownload the file
  197. DevMsg( 2, "%s -- Requesting download of preview image (%ld != %ld): %s -> %s\n", info.m_sGlobalName.Get(), lFileTime, lUpdatedAtStamp, info.m_sPreviewImage.Get(), info.m_sPreviewImageLocalFile.Get() );
  198. new CHelperStreamDownloadUrlToLocalFile( info.m_sPreviewImage.Get(), info.m_sPreviewImageLocalFile.Get(), lUpdatedAtStamp, pStreamPanel );
  199. }
  200. else
  201. {
  202. DevMsg( 2, "%s -- Preview image is up to date (%ld): %s -> %s\n", info.m_sGlobalName.Get(), lUpdatedAtStamp, info.m_sPreviewImage.Get(), info.m_sPreviewImageLocalFile.Get() );
  203. pStreamPanel->InvalidateLayout();
  204. }
  205. }
  206. else
  207. {
  208. DevMsg( 2, "%s -- No preview image\n", info.m_sGlobalName.Get() );
  209. info.m_sPreviewImageSF = cl_streams_image_sfurl.GetString();
  210. }
  211. }
  212. //////////////////////////////////////////////////////////////////////////
  213. //
  214. //
  215. static CTFStreamManager g_streamManager;
  216. CTFStreamManager* StreamManager()
  217. {
  218. return &g_streamManager;
  219. }
  220. CTFStreamManager::CTFStreamManager()
  221. {
  222. m_dblTimeStampLastUpdate = 0;
  223. m_hHTTPRequestHandle = NULL;
  224. m_hHTTPRequestHandleTwitchTv = NULL;
  225. m_pLoadingAccount = NULL;
  226. }
  227. CTFStreamManager::~CTFStreamManager()
  228. {
  229. m_vecTwitchTvAccounts.PurgeAndDeleteElements();
  230. }
  231. bool CTFStreamManager::Init()
  232. {
  233. RequestTopStreams();
  234. return true;
  235. }
  236. void CTFStreamManager::Update( float frametime )
  237. {
  238. // Cleanup downloaders
  239. if ( s_arrDeleteCHelperStreamDownloadUrlToLocalFile.Count() )
  240. {
  241. FOR_EACH_VEC( s_arrDeleteCHelperStreamDownloadUrlToLocalFile, iDownloader )
  242. {
  243. delete s_arrDeleteCHelperStreamDownloadUrlToLocalFile[iDownloader];
  244. }
  245. s_arrDeleteCHelperStreamDownloadUrlToLocalFile.RemoveAll();
  246. }
  247. UpdateTwitchTvAccounts();
  248. }
  249. void CTFStreamManager::RequestTopStreams()
  250. {
  251. // Check if it's time to update
  252. if ( !m_dblTimeStampLastUpdate || ( Plat_FloatTime() - m_dblTimeStampLastUpdate > cl_streams_refresh_interval.GetFloat() ) )
  253. {
  254. m_dblTimeStampLastUpdate = Plat_FloatTime();
  255. if ( !m_hHTTPRequestHandle && steamapicontext && steamapicontext->SteamHTTP() )
  256. {
  257. //
  258. // Create HTTP download job
  259. //
  260. m_hHTTPRequestHandle = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, cl_streams_request_url.GetString() );
  261. steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "Accept", cl_streams_request_accept.GetString() );
  262. steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "Client-ID", "b7816vx0i8sng8bwy9es0dirdcsy3im" );
  263. DevMsg( "Requesting twitch.tv streams update...\n" );
  264. SteamAPICall_t hCall = NULL;
  265. if ( m_hHTTPRequestHandle && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandle, &hCall ) && hCall )
  266. {
  267. m_CallbackOnHTTPRequestCompleted.Set( hCall, this, &CTFStreamManager::Steam_OnHTTPRequestCompletedStreams );
  268. }
  269. else
  270. {
  271. if ( m_hHTTPRequestHandle )
  272. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle );
  273. m_hHTTPRequestHandle = NULL;
  274. }
  275. }
  276. }
  277. }
  278. static int Helper_SortStreamsByViewersCount( const CStreamInfo *a, const CStreamInfo *b )
  279. {
  280. if ( a->m_numViewers != b->m_numViewers )
  281. return ( a->m_numViewers > b->m_numViewers ) ? -1 : 1;
  282. return Q_stricmp( a->m_sGlobalName.Get(), b->m_sGlobalName.Get() );
  283. }
  284. static void Helper_ConvertLanguageToCountryCode( CUtlString &s )
  285. {
  286. if ( !Q_stricmp(s, "en") )
  287. s = "gb";
  288. }
  289. void CTFStreamManager::Steam_OnHTTPRequestCompletedStreams( HTTPRequestCompleted_t *p, bool bError )
  290. {
  291. if ( !m_hHTTPRequestHandle || ( p->m_hRequest != m_hHTTPRequestHandle ) )
  292. return;
  293. uint32 unBytes = 0;
  294. if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 )
  295. {
  296. DevMsg( "Request for twitch.tv streams succeeded (code: %u, size: %u)...\n", p ? p->m_eStatusCode : 0, unBytes );
  297. CUtlVector< CStreamInfo > arrStreamInfos;
  298. CUtlBuffer bufFile;
  299. bufFile.EnsureCapacity( unBytes );
  300. if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) )
  301. {
  302. bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes );
  303. if ( cl_streams_write_response_file.GetString()[0] )
  304. {
  305. g_pFullFileSystem->WriteFile( cl_streams_write_response_file.GetString(), "GAME", bufFile );
  306. }
  307. // Parse JSON from the received file
  308. GCSDK::CWebAPIValues *pValues = GCSDK::CWebAPIValues::ParseJSON( bufFile );
  309. if ( pValues )
  310. {
  311. if ( GCSDK::CWebAPIValues *pvStreams = pValues->FindChild( "streams" ) )
  312. {
  313. for ( GCSDK::CWebAPIValues *pvStream = pvStreams->GetFirstChild(); pvStream; pvStream = pvStream->GetNextChild() )
  314. {
  315. #if 0 // this is the code to print JSON output as DevMsgs to figure out which element is where in the response
  316. DevMsg( "----STREAM----\n" );
  317. for ( GCSDK::CWebAPIValues *pTest = pvStream->GetFirstChild(); pTest; pTest = pTest->GetNextChild() )
  318. {
  319. CUtlString sValueText;
  320. pTest->GetStringValue( sValueText );
  321. DevMsg( "child: %s = (%u) %s\n", pTest->GetName(), pTest->GetUInt32Value(), sValueText.Get() );
  322. for ( GCSDK::CWebAPIValues *pTest2 = pTest->GetFirstChild(); pTest2; pTest2 = pTest2->GetNextChild() )
  323. {
  324. pTest2->GetStringValue( sValueText );
  325. DevMsg( " child2: %s = (%u) %s\n", pTest2->GetName(), pTest2->GetUInt32Value(), sValueText.Get() );
  326. }
  327. }
  328. #endif
  329. CStreamInfo info;
  330. info.m_numViewers = pvStream->GetChildUInt32Value( "viewers" );
  331. if ( GCSDK::CWebAPIValues *pChannel = pvStream->FindChild( "channel" ) )
  332. {
  333. pChannel->GetChildStringValue( info.m_sGlobalName, "name", "" );
  334. pChannel->GetChildStringValue( info.m_sDisplayName, "display_name", "" );
  335. pChannel->GetChildStringValue( info.m_sTextDescription, "status", "" );
  336. pChannel->GetChildStringValue( info.m_sLanguage, "language", "" ); // MISSING
  337. Helper_ConvertLanguageToCountryCode( info.m_sLanguage );
  338. info.m_sCountry = info.m_sLanguage; // MISSING
  339. pChannel->GetChildStringValue( info.m_sUpdatedAtStamp, "updated_at", "" );
  340. if ( GCSDK::CWebAPIValues *pPreview = pvStream->FindChild( "preview" ) )
  341. {
  342. if ( pPreview->GetType() == GCSDK::k_EWebAPIValueType_String )
  343. pPreview->GetStringValue( info.m_sPreviewImage );
  344. else
  345. pPreview->GetChildStringValue( info.m_sPreviewImage, "medium", "" );
  346. }
  347. pChannel->GetChildStringValue( info.m_sVideoFeedUrl, "url", "" );
  348. }
  349. if ( ( info.m_numViewers > 0 ) &&
  350. !info.m_sGlobalName.IsEmpty() &&
  351. !info.m_sDisplayName.IsEmpty() &&
  352. !info.m_sTextDescription.IsEmpty() &&
  353. !info.m_sVideoFeedUrl.IsEmpty() )
  354. {
  355. //DevMsg( 2, "Channel: %s (%s, %u viewers) -- %s [[%s]]\n", info.m_sGlobalName.Get(), info.m_sDisplayName.Get(), info.m_numViewers, info.m_sTextDescription.Get(), info.m_sVideoFeedUrl.Get() );
  356. arrStreamInfos.AddToTail( info );
  357. }
  358. }
  359. }
  360. }
  361. delete pValues;
  362. }
  363. if ( arrStreamInfos.Count() )
  364. {
  365. arrStreamInfos.Sort( Helper_SortStreamsByViewersCount );
  366. if ( arrStreamInfos.Count() > cl_streams_request_count.GetInt() )
  367. arrStreamInfos.SetCountNonDestructively( cl_streams_request_count.GetInt() );
  368. m_streamInfoVec.Swap( arrStreamInfos ); // Set the new information live and notify all systems
  369. IGameEvent *event = gameeventmanager->CreateEvent( "top_streams_request_finished" );
  370. if ( event )
  371. {
  372. gameeventmanager->FireEventClientSide( event );
  373. }
  374. }
  375. }
  376. else
  377. {
  378. DevMsg( "Request for twitch.tv streams failed: %s (code: %u, size: %u)...\n",
  379. bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes );
  380. }
  381. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest );
  382. m_hHTTPRequestHandle = NULL;
  383. m_dblTimeStampLastUpdate = Plat_FloatTime(); // push the update counter to not update for a little bit
  384. }
  385. CStreamInfo* CTFStreamManager::GetStreamInfoByName( char const *szName )
  386. {
  387. if ( !szName )
  388. return NULL;
  389. for ( int idx = 0; idx < m_streamInfoVec.Count(); ++ idx )
  390. {
  391. if ( !V_strcmp( szName, m_streamInfoVec[idx].m_sGlobalName.Get() ) )
  392. return &m_streamInfoVec[idx];
  393. }
  394. return NULL;
  395. }
  396. TwitchTvAccountInfo_t* CTFStreamManager::GetTwitchTvAccountInfo( uint64 uiSteamID )
  397. {
  398. TwitchTvAccountInfo_t *pInfo = NULL;
  399. FOR_EACH_VEC( m_vecTwitchTvAccounts, i )
  400. {
  401. if ( m_vecTwitchTvAccounts[i]->m_uiSteamID == uiSteamID )
  402. {
  403. pInfo = m_vecTwitchTvAccounts[i];
  404. // Uncomment this if we really need to keep checking twitch status
  405. // info needs update if it's been longer than 300 secs from the last update
  406. /*if ( pInfo->m_dblTimeStampTwitchTvUpdate && ( Plat_FloatTime() - pInfo->m_dblTimeStampTwitchTvUpdate > 300 ) )
  407. {
  408. m_vecTwitchTvAccounts.Remove( i );
  409. pInfo = NULL;
  410. }*/
  411. break;
  412. }
  413. }
  414. // add one if not already on the list
  415. if ( !pInfo )
  416. {
  417. pInfo = new TwitchTvAccountInfo_t;
  418. if ( pInfo )
  419. {
  420. pInfo->m_uiSteamID = uiSteamID;
  421. pInfo->m_eTwitchTvState = k_ETwitchTvState_None;
  422. pInfo->m_dblTimeStampTwitchTvUpdate = 0;
  423. pInfo->m_uiTwitchTvUserId = 0;
  424. pInfo->m_sTwitchTvChannel = cl_streams_mytwitchtv_nolink.GetString();
  425. m_vecTwitchTvAccounts.AddToTail( pInfo );
  426. }
  427. }
  428. return pInfo;
  429. }
  430. void CTFStreamManager::UpdateTwitchTvAccounts()
  431. {
  432. if ( m_pLoadingAccount )
  433. return;
  434. // find the new account in the list to load
  435. FOR_EACH_VEC( m_vecTwitchTvAccounts, i )
  436. {
  437. if ( m_vecTwitchTvAccounts[i]->m_eTwitchTvState == k_ETwitchTvState_None )
  438. {
  439. m_pLoadingAccount = m_vecTwitchTvAccounts[i];
  440. break;
  441. }
  442. }
  443. // nothing needs to be loaded
  444. if ( !m_pLoadingAccount )
  445. return;
  446. // When requesting a refresh reset all known linking state
  447. m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Loading;
  448. m_pLoadingAccount->m_dblTimeStampTwitchTvUpdate = Plat_FloatTime();
  449. Assert( !m_hHTTPRequestHandleTwitchTv );
  450. if ( m_hHTTPRequestHandleTwitchTv ) return;
  451. //
  452. // Create HTTP download job
  453. //
  454. m_hHTTPRequestHandleTwitchTv = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, CFmtStr( "http://api.twitch.tv/api/steam/%llu", m_pLoadingAccount->m_uiSteamID ) );
  455. steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandleTwitchTv, "Accept", cl_streams_request_accept.GetString() );
  456. DevMsg( "Requesting twitch.tv account link...\n" );
  457. SteamAPICall_t hCall = NULL;
  458. if ( m_hHTTPRequestHandleTwitchTv && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandleTwitchTv, &hCall ) && hCall )
  459. {
  460. m_CallbackOnHTTPRequestCompletedTwitchTv.Set( hCall, this, &CTFStreamManager::Steam_OnHTTPRequestCompletedMyTwitchTv );
  461. }
  462. else
  463. {
  464. if ( m_hHTTPRequestHandleTwitchTv )
  465. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandleTwitchTv );
  466. m_hHTTPRequestHandleTwitchTv = NULL;
  467. }
  468. }
  469. void CTFStreamManager::Steam_OnHTTPRequestCompletedMyTwitchTv( HTTPRequestCompleted_t *p, bool bError )
  470. {
  471. if ( !m_hHTTPRequestHandleTwitchTv || ( p->m_hRequest != m_hHTTPRequestHandleTwitchTv ) )
  472. return;
  473. Assert( m_pLoadingAccount->m_eTwitchTvState == k_ETwitchTvState_Loading );
  474. m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Error;
  475. m_pLoadingAccount->m_sTwitchTvChannel.Clear();
  476. m_pLoadingAccount->m_uiTwitchTvUserId = 0ull;
  477. uint32 unBytes = 0;
  478. if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 )
  479. {
  480. DevMsg( "Request for twitch.tv account link succeeded (code: %u, size: %u)...\n", p ? p->m_eStatusCode : 0, unBytes );
  481. CUtlBuffer bufFile;
  482. bufFile.EnsureCapacity( unBytes );
  483. if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) )
  484. {
  485. bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes );
  486. if ( cl_streams_write_response_file.GetString()[0] )
  487. {
  488. g_pFullFileSystem->WriteFile( cl_streams_write_response_file.GetString(), "GAME", bufFile );
  489. }
  490. // Parse JSON from the received file
  491. GCSDK::CWebAPIValues *pValues = GCSDK::CWebAPIValues::ParseJSON( bufFile );
  492. if ( pValues )
  493. {
  494. pValues->GetChildStringValue( m_pLoadingAccount->m_sTwitchTvChannel, "name", "" );
  495. m_pLoadingAccount->m_uiTwitchTvUserId = pValues->GetChildUInt64Value( "_id" );
  496. if ( m_pLoadingAccount->m_sTwitchTvChannel.IsEmpty() )
  497. {
  498. m_pLoadingAccount->m_sTwitchTvChannel = cl_streams_mytwitchtv_nolink.GetString();
  499. m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_NoLink;
  500. }
  501. else
  502. {
  503. m_pLoadingAccount->m_sTwitchTvChannel = CFmtStr( "%s%s", cl_streams_mytwitchtv_channel.GetString(), m_pLoadingAccount->m_sTwitchTvChannel.Get() );
  504. m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Linked;
  505. }
  506. }
  507. delete pValues;
  508. }
  509. }
  510. else
  511. {
  512. DevMsg( "Request for twitch.tv account link failed: %s (code: %u, size: %u)...\n",
  513. bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes );
  514. }
  515. steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest );
  516. m_hHTTPRequestHandleTwitchTv = NULL;
  517. m_pLoadingAccount->m_dblTimeStampTwitchTvUpdate = Plat_FloatTime(); // push the update counter to not update for a little bit
  518. // done loading
  519. m_pLoadingAccount = NULL;
  520. }
  521. CTFStreamPanel::CTFStreamPanel( Panel *parent, const char *panelName ) : EditablePanel( parent, panelName )
  522. {
  523. vgui::HScheme scheme = vgui::scheme()->LoadSchemeFromFileEx( enginevgui->GetPanel( PANEL_CLIENTDLL ), "resource/ClientScheme.res", "ClientScheme");
  524. SetScheme(scheme);
  525. m_pPreviewImage = new ImagePanel( this, "PreviewImage" );
  526. }
  527. void CTFStreamPanel::ApplySchemeSettings( IScheme *pScheme )
  528. {
  529. BaseClass::ApplySchemeSettings( pScheme );
  530. LoadControlSettings( "Resource/UI/StreamPanel.res" );
  531. }
  532. void CTFStreamPanel::PerformLayout()
  533. {
  534. BaseClass::PerformLayout();
  535. UpdatePanels();
  536. }
  537. void CTFStreamPanel::OnCommand( const char *command )
  538. {
  539. if ( FStrEq( command, "stream" ) )
  540. {
  541. CStreamInfo *pInfo = GetStreamInfo();
  542. if ( pInfo )
  543. {
  544. vgui::system()->ShellExecute( "open", CFmtStr( "%s%s", cl_streams_mytwitchtv_channel.GetString(), pInfo->m_sDisplayName.Get() ) );
  545. }
  546. }
  547. else
  548. {
  549. BaseClass::OnCommand( command );
  550. }
  551. }
  552. CStreamInfo *CTFStreamPanel::GetStreamInfo() const
  553. {
  554. if ( m_strStreamInfoGlobalName.IsEmpty() )
  555. return NULL;
  556. return g_streamManager.GetStreamInfoByName( m_strStreamInfoGlobalName.Get() );
  557. }
  558. void CTFStreamPanel::UpdatePanels()
  559. {
  560. CStreamInfo *pInfo = GetStreamInfo();
  561. if ( pInfo )
  562. {
  563. SetDialogVariable( "display_name", pInfo->m_sDisplayName.Get() );
  564. SetDialogVariable( "viewer_count", CFmtStr( "%d viewers", pInfo->m_numViewers ) );
  565. SetDialogVariable( "text_description", pInfo->m_sTextDescription.Get() );
  566. SetPreviewImage( pInfo->m_sPreviewImageLocalFile.Get() );
  567. Panel* pLoadingPanel = FindChildByName( "LoadingPanel" );
  568. if ( pLoadingPanel )
  569. {
  570. pLoadingPanel->SetVisible( false );
  571. }
  572. }
  573. else
  574. {
  575. SetDialogVariable( "display_name", "" );
  576. SetDialogVariable( "viewer_count", "" );
  577. SetDialogVariable( "text_description", "" );
  578. SetPreviewImage( NULL );
  579. }
  580. Panel* pStreamButton = FindChildByName( "Stream_URLButton" );
  581. if ( pStreamButton )
  582. {
  583. pStreamButton->SetEnabled( pInfo != NULL );
  584. }
  585. }
  586. void CTFStreamPanel::SetPreviewImage( const char *pszPreviewImageFile )
  587. {
  588. // clean up old image if there's one
  589. m_pPreviewImage->EvictImage();
  590. if ( !pszPreviewImageFile )
  591. {
  592. m_pPreviewImage->SetImage( (vgui::IImage *)0 );
  593. return;
  594. }
  595. char szImageAbsPath[MAX_PATH];
  596. if ( !GenerateFullPath( pszPreviewImageFile, "MOD", szImageAbsPath, ARRAYSIZE( szImageAbsPath ) ) )
  597. {
  598. Warning( "Failed to GenerateFullPath %s\n", pszPreviewImageFile );
  599. return;
  600. }
  601. Bitmap_t image;
  602. ConversionErrorType nErrorCode = ImgUtl_LoadBitmap( szImageAbsPath, image );
  603. if ( nErrorCode != CE_SUCCESS )
  604. {
  605. m_pPreviewImage->SetImage( (vgui::IImage *)0 );
  606. return;
  607. }
  608. int wide, tall;
  609. BitmapImage *pBitmapImage = new BitmapImage;
  610. pBitmapImage->SetBitmap( image );
  611. pBitmapImage->GetSize( wide, tall );
  612. // The ImagePanel needs to know the scaling factor for the BitmapImage, to
  613. // center it when it's going to be rendered, and then the BitmapImage needs
  614. // to know how big it should be rendered.
  615. float flPanelWidthScale = static_cast<float>( m_pPreviewImage->GetWide() ) / wide;
  616. m_pPreviewImage->SetShouldCenterImage( false );
  617. m_pPreviewImage->SetShouldScaleImage( true );
  618. m_pPreviewImage->SetScaleAmount( flPanelWidthScale );
  619. float flSubRectWidthScale = static_cast<float>( wide ) / image.Width();
  620. //float flSubRectHeightScale = static_cast<float>( tall ) / image.Height();
  621. pBitmapImage->SetRenderSize( flSubRectWidthScale * m_pPreviewImage->GetWide(), flSubRectWidthScale * flPanelWidthScale * tall );
  622. m_pPreviewImage->SetImage( pBitmapImage );
  623. float flPanelHeightScale = static_cast<float>( image.Height() ) / image.Width();
  624. m_pPreviewImage->SetSize( m_pPreviewImage->GetWide(), flPanelHeightScale * m_pPreviewImage->GetWide() );
  625. }
  626. CTFStreamListPanel::CTFStreamListPanel( Panel *parent, const char *panelName ) : EditablePanel( parent, panelName )
  627. {
  628. for ( int i=0; i<ARRAYSIZE( m_arrStreamPanels ); ++i )
  629. {
  630. m_arrStreamPanels[i] = new CTFStreamPanel( this, CFmtStr( "Stream%d", i + 1 ) );
  631. }
  632. ListenForGameEvent( "top_streams_request_finished" );
  633. }
  634. void CTFStreamListPanel::ApplySchemeSettings( IScheme *pScheme )
  635. {
  636. BaseClass::ApplySchemeSettings( pScheme );
  637. LoadControlSettings( "Resource/UI/StreamListPanel.res" );
  638. }
  639. void CTFStreamListPanel::OnThink()
  640. {
  641. // this will fire "top_streams_request_finished" event when the job is done
  642. g_streamManager.RequestTopStreams();
  643. }
  644. void CTFStreamListPanel::OnCommand( const char *command )
  645. {
  646. if ( FStrEq( command, "hide_streams" ) )
  647. {
  648. SetVisible( false );
  649. }
  650. else if ( FStrEq( command, "view_more" ) )
  651. {
  652. vgui::system()->ShellExecute( "open", "https://www.twitch.tv/directory/game/Team%20Fortress%202" );
  653. }
  654. else
  655. {
  656. BaseClass::OnCommand( command );
  657. }
  658. }
  659. void CTFStreamListPanel::FireGameEvent( IGameEvent *event )
  660. {
  661. const char *pszEventName = event->GetName();
  662. if ( FStrEq( pszEventName, "top_streams_request_finished" ) )
  663. {
  664. // update each stream panel
  665. for ( int i=0; i<ARRAYSIZE( m_arrStreamPanels ); ++i )
  666. {
  667. bool bVisible = i < g_streamManager.GetStreamInfoVec().Count();
  668. m_arrStreamPanels[i]->SetVisible( bVisible );
  669. if ( bVisible )
  670. {
  671. Helper_ConfigureStreamInfoPreviewImages( g_streamManager.GetStreamInfoVec()[i], m_arrStreamPanels[i] );
  672. }
  673. }
  674. }
  675. }