//========= Copyright (C) 1996-2013, Valve Corporation, All rights reserved. ============// // // Purpose: Helper for access to news // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "gc_clientsystem.h" #include "filesystem.h" #include "workshop/ugc_utils.h" #include "econ/econ_controls.h" #include "ienginevgui.h" #include "vgui/ISystem.h" #include "vgui_controls/ImagePanel.h" #include "vgui_bitmapimage.h" #include "bitmap/bitmap.h" #include "imageutils.h" #include "tier2/fileutils.h" #include "checksum_sha1.h" //#include "matchmaking/imatchframework.h" //#include "engine/inetsupport.h" #include "rtime.h" #include "tf_streams.h" // memdbgon must be the last include file in a .cpp file!!! #include // GC hello information //extern CMsgGCCStrike15_v2_MatchmakingGC2ClientHello g_GC2ClientHello; ConVar cl_streams_request_url( "cl_streams_request_url", "https://api.twitch.tv/kraken/streams?game=Team%20Fortress%202&limit=5" , FCVAR_DEVELOPMENTONLY, "Number of streams requested for display" ); ConVar cl_streams_request_accept( "cl_streams_request_accept", "application/vnd.twitchtv.v3+json", FCVAR_DEVELOPMENTONLY, "Header for api request" ); ConVar cl_streams_image_sfurl( "cl_streams_image_sfurl", "img://loadjpeg:(320x200):" , FCVAR_DEVELOPMENTONLY, "Format of Scaleform image representing the stream" ); ConVar cl_streams_request_count( "cl_streams_request_count", "6", FCVAR_DEVELOPMENTONLY, "How many streams are displayed in main menu" ); ConVar cl_streams_refresh_interval( "cl_streams_refresh_interval", "60", FCVAR_DEVELOPMENTONLY, "How often to refresh streams list" ); ConVar cl_streams_write_response_file( "cl_streams_write_response_file", "", FCVAR_DEVELOPMENTONLY, "When set will save streams info file for diagnostics" ); ConVar cl_streams_override_global_version( "cl_streams_override_global_version", "", FCVAR_DEVELOPMENTONLY, "When set will override global API version" ); ConVar cl_streams_mytwitchtv_nolink( "cl_streams_mytwitchtv_nolink", "https://www.twitch.tv/settings/connections", FCVAR_DEVELOPMENTONLY, "Twitch.tv account linking URL" ); ConVar cl_streams_mytwitchtv_channel( "cl_streams_mytwitchtv_channel", "https://www.twitch.tv/", FCVAR_DEVELOPMENTONLY, "Twitch.tv account channel URL" ); static const char *s_pszCacheImagePath = "streams"; ////////////////////////////////////////////////////////////////////////// // // Files downloader // class CHelperStreamDownloadUrlToLocalFile; static CUtlVector< CHelperStreamDownloadUrlToLocalFile * > s_arrDeleteCHelperStreamDownloadUrlToLocalFile; class CHelperStreamDownloadUrlToLocalFile { public: CHelperStreamDownloadUrlToLocalFile( char const *szUrlGet, char const *szLocalFile, long lTimeStampLocal, CTFStreamPanel *pStreamPanel ) { m_sUrlGet = szUrlGet; m_sLocalFile = szLocalFile; m_lTimestampLocal = lTimeStampLocal; m_hHTTPRequestHandle = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, m_sUrlGet.Get() ); SteamAPICall_t hCall = NULL; if ( m_hHTTPRequestHandle && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandle, &hCall ) && hCall ) { m_CallbackOnHTTPRequestCompleted.Set( hCall, this, &CHelperStreamDownloadUrlToLocalFile::Steam_OnHTTPRequestCompleted ); } else { if ( m_hHTTPRequestHandle ) steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle ); m_hHTTPRequestHandle = NULL; } m_hStreamPanel = pStreamPanel; } private: CUtlString m_sUrlGet; CUtlString m_sLocalFile; long m_lTimestampLocal; HTTPRequestHandle m_hHTTPRequestHandle; CCallResult< CHelperStreamDownloadUrlToLocalFile, HTTPRequestCompleted_t > m_CallbackOnHTTPRequestCompleted; DHANDLE m_hStreamPanel; void Steam_OnHTTPRequestCompleted( HTTPRequestCompleted_t *p, bool bError ) { if ( !m_hHTTPRequestHandle || ( p->m_hRequest != m_hHTTPRequestHandle ) ) return; uint32 unBytes = 0; if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 ) { DevMsg( "Request for '%s' succeeded (code: %u, size: %u)...\n", m_sUrlGet.Get(), p ? p->m_eStatusCode : 0, unBytes ); CUtlBuffer bufFile; bufFile.EnsureCapacity( unBytes ); if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) ) { bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes ); g_pFullFileSystem->WriteFile( m_sLocalFile.Get(), "GAME", bufFile ); UGC_SetFileTime( m_sLocalFile.Get(), m_lTimestampLocal ); } if ( m_hStreamPanel.Get() ) { m_hStreamPanel.Get()->InvalidateLayout(); } } else { DevMsg( "Request for '%s' failed '%s' (code: %u, size: %u)...\n", m_sUrlGet.Get(), bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes ); } steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest ); m_hHTTPRequestHandle = NULL; s_arrDeleteCHelperStreamDownloadUrlToLocalFile.AddToTail( this ); } }; ////////////////////////////////////////////////////////////////////////// // // Helper functions // static long Helper_ParseUpdatedTimestamp( char const *pchParseUpdatedAt ) { // Twitch.tv format: "2013-03-04T05:27:27Z" return CRTime::RTime32FromRFC3339UTCString( pchParseUpdatedAt ); } static void Helper_ConfigureStreamInfoPreviewImages( CStreamInfo &info, CTFStreamPanel *pStreamPanel ) { // store global name for the info in the panel to ask for it when the image is ready to be displayed pStreamPanel->SetGlobalName( info.m_sGlobalName ); char chPreviewImageLocal[ 2*MAX_PATH + 1 ] = {}; char *pchLocalCur = chPreviewImageLocal, *pchLocalEnd = chPreviewImageLocal + Q_ARRAYSIZE( chPreviewImageLocal ) - 1; for ( char const *pch = info.m_sPreviewImage.Get(); *pch; ++ pch ) { if ( pchLocalEnd - pchLocalCur < 4 ) break; if ( ( ( pch[0] >= 'a' ) && ( pch[0] <= 'z' ) ) || ( ( pch[0] >= 'A' ) && ( pch[0] <= 'Z' ) ) || ( ( pch[0] >= '0' ) && ( pch[0] <= '9' ) ) || ( pch[0] == '-' ) || ( pch[0] == '.' ) ) { * ( pchLocalCur ++ ) = *pch; } else { * ( pchLocalCur ++ ) = '_'; int iHigh = ( ( ( unsigned char )( *pch ) & 0xF0u ) >> 4 ) & 0xF; if ( iHigh >= 0 && iHigh <= 9 ) * ( pchLocalCur ++ ) = '0' + iHigh; else * ( pchLocalCur ++ ) = 'A' + iHigh; int iLow = ( ( ( unsigned char )( *pch ) & 0xFu ) ); if ( iLow >= 0 && iLow <= 9 ) * ( pchLocalCur ++ ) = '0' + iLow; else if ( iLow >= 10 && iLow <= 15 ) * ( pchLocalCur ++ ) = 'A' + iLow - 10; * ( pchLocalCur ++ ) = '_'; } } if ( chPreviewImageLocal[0] ) { // We might need to download the file, so make sure directory structure is valid static bool s_bCreateDirHierarchyDone = false; if ( !s_bCreateDirHierarchyDone ) { s_bCreateDirHierarchyDone = true; // Cleanup old cached files if ( long lDirectoryTime = g_pFullFileSystem->GetFileTime( s_pszCacheImagePath, "GAME" ) ) { FileFindHandle_t hFind = NULL; int numRemove = 0; for ( char const *szFileName = g_pFullFileSystem->FindFirst( CFmtStr( "%s/*", s_pszCacheImagePath ), &hFind ); szFileName && *szFileName; szFileName = g_pFullFileSystem->FindNext( hFind ) ) { if ( !Q_strcmp( ".", szFileName ) || !Q_strcmp( "..", szFileName ) ) continue; CFmtStr fmtFilename( "%s/%s", s_pszCacheImagePath, szFileName ); long lFileTime = g_pFullFileSystem->GetFileTime( fmtFilename, "GAME" ); if ( ( lFileTime >= lDirectoryTime - 72*3600 ) && ( lFileTime <= lDirectoryTime + 72*3600 ) ) { // DevMsg( "Keeping file %s (%u : %u)\n", fmtFilename.Access(), lFileTime, lDirectoryTime ); continue; } else { ++ numRemove; g_pFullFileSystem->RemoveFile( fmtFilename, "GAME" ); } } DevMsg( 2, "Streams preview cache evicted %u files\n", numRemove ); g_pFullFileSystem->FindClose( hFind ); } g_pFullFileSystem->CreateDirHierarchy( s_pszCacheImagePath, "GAME" ); } // // Work with the file cache // CFmtStr fmtLocalFile( "%s/%s", s_pszCacheImagePath, chPreviewImageLocal ); info.m_sPreviewImageLocalFile = fmtLocalFile.Access(); CFmtStr fmtPreviewSF( "%sstreams/%s", cl_streams_image_sfurl.GetString(), chPreviewImageLocal ); info.m_sPreviewImageSF = fmtPreviewSF.Access(); // See if we need to download that file long lFileTime = g_pFullFileSystem->GetFileTime( fmtLocalFile.Access(), "GAME" ); // Parse "updated_at" attribute: long lUpdatedAtStamp = Helper_ParseUpdatedTimestamp( info.m_sUpdatedAtStamp.Get() ); if ( lFileTime != lUpdatedAtStamp ) { // Redownload the file 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() ); new CHelperStreamDownloadUrlToLocalFile( info.m_sPreviewImage.Get(), info.m_sPreviewImageLocalFile.Get(), lUpdatedAtStamp, pStreamPanel ); } else { 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() ); pStreamPanel->InvalidateLayout(); } } else { DevMsg( 2, "%s -- No preview image\n", info.m_sGlobalName.Get() ); info.m_sPreviewImageSF = cl_streams_image_sfurl.GetString(); } } ////////////////////////////////////////////////////////////////////////// // // static CTFStreamManager g_streamManager; CTFStreamManager* StreamManager() { return &g_streamManager; } CTFStreamManager::CTFStreamManager() { m_dblTimeStampLastUpdate = 0; m_hHTTPRequestHandle = NULL; m_hHTTPRequestHandleTwitchTv = NULL; m_pLoadingAccount = NULL; } CTFStreamManager::~CTFStreamManager() { m_vecTwitchTvAccounts.PurgeAndDeleteElements(); } bool CTFStreamManager::Init() { RequestTopStreams(); return true; } void CTFStreamManager::Update( float frametime ) { // Cleanup downloaders if ( s_arrDeleteCHelperStreamDownloadUrlToLocalFile.Count() ) { FOR_EACH_VEC( s_arrDeleteCHelperStreamDownloadUrlToLocalFile, iDownloader ) { delete s_arrDeleteCHelperStreamDownloadUrlToLocalFile[iDownloader]; } s_arrDeleteCHelperStreamDownloadUrlToLocalFile.RemoveAll(); } UpdateTwitchTvAccounts(); } void CTFStreamManager::RequestTopStreams() { // Check if it's time to update if ( !m_dblTimeStampLastUpdate || ( Plat_FloatTime() - m_dblTimeStampLastUpdate > cl_streams_refresh_interval.GetFloat() ) ) { m_dblTimeStampLastUpdate = Plat_FloatTime(); if ( !m_hHTTPRequestHandle && steamapicontext && steamapicontext->SteamHTTP() ) { // // Create HTTP download job // m_hHTTPRequestHandle = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, cl_streams_request_url.GetString() ); steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "Accept", cl_streams_request_accept.GetString() ); steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "Client-ID", "b7816vx0i8sng8bwy9es0dirdcsy3im" ); DevMsg( "Requesting twitch.tv streams update...\n" ); SteamAPICall_t hCall = NULL; if ( m_hHTTPRequestHandle && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandle, &hCall ) && hCall ) { m_CallbackOnHTTPRequestCompleted.Set( hCall, this, &CTFStreamManager::Steam_OnHTTPRequestCompletedStreams ); } else { if ( m_hHTTPRequestHandle ) steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle ); m_hHTTPRequestHandle = NULL; } } } } static int Helper_SortStreamsByViewersCount( const CStreamInfo *a, const CStreamInfo *b ) { if ( a->m_numViewers != b->m_numViewers ) return ( a->m_numViewers > b->m_numViewers ) ? -1 : 1; return Q_stricmp( a->m_sGlobalName.Get(), b->m_sGlobalName.Get() ); } static void Helper_ConvertLanguageToCountryCode( CUtlString &s ) { if ( !Q_stricmp(s, "en") ) s = "gb"; } void CTFStreamManager::Steam_OnHTTPRequestCompletedStreams( HTTPRequestCompleted_t *p, bool bError ) { if ( !m_hHTTPRequestHandle || ( p->m_hRequest != m_hHTTPRequestHandle ) ) return; uint32 unBytes = 0; if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 ) { DevMsg( "Request for twitch.tv streams succeeded (code: %u, size: %u)...\n", p ? p->m_eStatusCode : 0, unBytes ); CUtlVector< CStreamInfo > arrStreamInfos; CUtlBuffer bufFile; bufFile.EnsureCapacity( unBytes ); if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) ) { bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes ); if ( cl_streams_write_response_file.GetString()[0] ) { g_pFullFileSystem->WriteFile( cl_streams_write_response_file.GetString(), "GAME", bufFile ); } // Parse JSON from the received file GCSDK::CWebAPIValues *pValues = GCSDK::CWebAPIValues::ParseJSON( bufFile ); if ( pValues ) { if ( GCSDK::CWebAPIValues *pvStreams = pValues->FindChild( "streams" ) ) { for ( GCSDK::CWebAPIValues *pvStream = pvStreams->GetFirstChild(); pvStream; pvStream = pvStream->GetNextChild() ) { #if 0 // this is the code to print JSON output as DevMsgs to figure out which element is where in the response DevMsg( "----STREAM----\n" ); for ( GCSDK::CWebAPIValues *pTest = pvStream->GetFirstChild(); pTest; pTest = pTest->GetNextChild() ) { CUtlString sValueText; pTest->GetStringValue( sValueText ); DevMsg( "child: %s = (%u) %s\n", pTest->GetName(), pTest->GetUInt32Value(), sValueText.Get() ); for ( GCSDK::CWebAPIValues *pTest2 = pTest->GetFirstChild(); pTest2; pTest2 = pTest2->GetNextChild() ) { pTest2->GetStringValue( sValueText ); DevMsg( " child2: %s = (%u) %s\n", pTest2->GetName(), pTest2->GetUInt32Value(), sValueText.Get() ); } } #endif CStreamInfo info; info.m_numViewers = pvStream->GetChildUInt32Value( "viewers" ); if ( GCSDK::CWebAPIValues *pChannel = pvStream->FindChild( "channel" ) ) { pChannel->GetChildStringValue( info.m_sGlobalName, "name", "" ); pChannel->GetChildStringValue( info.m_sDisplayName, "display_name", "" ); pChannel->GetChildStringValue( info.m_sTextDescription, "status", "" ); pChannel->GetChildStringValue( info.m_sLanguage, "language", "" ); // MISSING Helper_ConvertLanguageToCountryCode( info.m_sLanguage ); info.m_sCountry = info.m_sLanguage; // MISSING pChannel->GetChildStringValue( info.m_sUpdatedAtStamp, "updated_at", "" ); if ( GCSDK::CWebAPIValues *pPreview = pvStream->FindChild( "preview" ) ) { if ( pPreview->GetType() == GCSDK::k_EWebAPIValueType_String ) pPreview->GetStringValue( info.m_sPreviewImage ); else pPreview->GetChildStringValue( info.m_sPreviewImage, "medium", "" ); } pChannel->GetChildStringValue( info.m_sVideoFeedUrl, "url", "" ); } if ( ( info.m_numViewers > 0 ) && !info.m_sGlobalName.IsEmpty() && !info.m_sDisplayName.IsEmpty() && !info.m_sTextDescription.IsEmpty() && !info.m_sVideoFeedUrl.IsEmpty() ) { //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() ); arrStreamInfos.AddToTail( info ); } } } } delete pValues; } if ( arrStreamInfos.Count() ) { arrStreamInfos.Sort( Helper_SortStreamsByViewersCount ); if ( arrStreamInfos.Count() > cl_streams_request_count.GetInt() ) arrStreamInfos.SetCountNonDestructively( cl_streams_request_count.GetInt() ); m_streamInfoVec.Swap( arrStreamInfos ); // Set the new information live and notify all systems IGameEvent *event = gameeventmanager->CreateEvent( "top_streams_request_finished" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } } } else { DevMsg( "Request for twitch.tv streams failed: %s (code: %u, size: %u)...\n", bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes ); } steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest ); m_hHTTPRequestHandle = NULL; m_dblTimeStampLastUpdate = Plat_FloatTime(); // push the update counter to not update for a little bit } CStreamInfo* CTFStreamManager::GetStreamInfoByName( char const *szName ) { if ( !szName ) return NULL; for ( int idx = 0; idx < m_streamInfoVec.Count(); ++ idx ) { if ( !V_strcmp( szName, m_streamInfoVec[idx].m_sGlobalName.Get() ) ) return &m_streamInfoVec[idx]; } return NULL; } TwitchTvAccountInfo_t* CTFStreamManager::GetTwitchTvAccountInfo( uint64 uiSteamID ) { TwitchTvAccountInfo_t *pInfo = NULL; FOR_EACH_VEC( m_vecTwitchTvAccounts, i ) { if ( m_vecTwitchTvAccounts[i]->m_uiSteamID == uiSteamID ) { pInfo = m_vecTwitchTvAccounts[i]; // Uncomment this if we really need to keep checking twitch status // info needs update if it's been longer than 300 secs from the last update /*if ( pInfo->m_dblTimeStampTwitchTvUpdate && ( Plat_FloatTime() - pInfo->m_dblTimeStampTwitchTvUpdate > 300 ) ) { m_vecTwitchTvAccounts.Remove( i ); pInfo = NULL; }*/ break; } } // add one if not already on the list if ( !pInfo ) { pInfo = new TwitchTvAccountInfo_t; if ( pInfo ) { pInfo->m_uiSteamID = uiSteamID; pInfo->m_eTwitchTvState = k_ETwitchTvState_None; pInfo->m_dblTimeStampTwitchTvUpdate = 0; pInfo->m_uiTwitchTvUserId = 0; pInfo->m_sTwitchTvChannel = cl_streams_mytwitchtv_nolink.GetString(); m_vecTwitchTvAccounts.AddToTail( pInfo ); } } return pInfo; } void CTFStreamManager::UpdateTwitchTvAccounts() { if ( m_pLoadingAccount ) return; // find the new account in the list to load FOR_EACH_VEC( m_vecTwitchTvAccounts, i ) { if ( m_vecTwitchTvAccounts[i]->m_eTwitchTvState == k_ETwitchTvState_None ) { m_pLoadingAccount = m_vecTwitchTvAccounts[i]; break; } } // nothing needs to be loaded if ( !m_pLoadingAccount ) return; // When requesting a refresh reset all known linking state m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Loading; m_pLoadingAccount->m_dblTimeStampTwitchTvUpdate = Plat_FloatTime(); Assert( !m_hHTTPRequestHandleTwitchTv ); if ( m_hHTTPRequestHandleTwitchTv ) return; // // Create HTTP download job // m_hHTTPRequestHandleTwitchTv = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, CFmtStr( "http://api.twitch.tv/api/steam/%llu", m_pLoadingAccount->m_uiSteamID ) ); steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandleTwitchTv, "Accept", cl_streams_request_accept.GetString() ); DevMsg( "Requesting twitch.tv account link...\n" ); SteamAPICall_t hCall = NULL; if ( m_hHTTPRequestHandleTwitchTv && steamapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandleTwitchTv, &hCall ) && hCall ) { m_CallbackOnHTTPRequestCompletedTwitchTv.Set( hCall, this, &CTFStreamManager::Steam_OnHTTPRequestCompletedMyTwitchTv ); } else { if ( m_hHTTPRequestHandleTwitchTv ) steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandleTwitchTv ); m_hHTTPRequestHandleTwitchTv = NULL; } } void CTFStreamManager::Steam_OnHTTPRequestCompletedMyTwitchTv( HTTPRequestCompleted_t *p, bool bError ) { if ( !m_hHTTPRequestHandleTwitchTv || ( p->m_hRequest != m_hHTTPRequestHandleTwitchTv ) ) return; Assert( m_pLoadingAccount->m_eTwitchTvState == k_ETwitchTvState_Loading ); m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Error; m_pLoadingAccount->m_sTwitchTvChannel.Clear(); m_pLoadingAccount->m_uiTwitchTvUserId = 0ull; uint32 unBytes = 0; if ( !bError && p && steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( p->m_hRequest, &unBytes ) && unBytes != 0 ) { DevMsg( "Request for twitch.tv account link succeeded (code: %u, size: %u)...\n", p ? p->m_eStatusCode : 0, unBytes ); CUtlBuffer bufFile; bufFile.EnsureCapacity( unBytes ); if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData( p->m_hRequest,(uint8*)bufFile.Base(), unBytes ) ) { bufFile.SeekPut( bufFile.SEEK_HEAD, unBytes ); if ( cl_streams_write_response_file.GetString()[0] ) { g_pFullFileSystem->WriteFile( cl_streams_write_response_file.GetString(), "GAME", bufFile ); } // Parse JSON from the received file GCSDK::CWebAPIValues *pValues = GCSDK::CWebAPIValues::ParseJSON( bufFile ); if ( pValues ) { pValues->GetChildStringValue( m_pLoadingAccount->m_sTwitchTvChannel, "name", "" ); m_pLoadingAccount->m_uiTwitchTvUserId = pValues->GetChildUInt64Value( "_id" ); if ( m_pLoadingAccount->m_sTwitchTvChannel.IsEmpty() ) { m_pLoadingAccount->m_sTwitchTvChannel = cl_streams_mytwitchtv_nolink.GetString(); m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_NoLink; } else { m_pLoadingAccount->m_sTwitchTvChannel = CFmtStr( "%s%s", cl_streams_mytwitchtv_channel.GetString(), m_pLoadingAccount->m_sTwitchTvChannel.Get() ); m_pLoadingAccount->m_eTwitchTvState = k_ETwitchTvState_Linked; } } delete pValues; } } else { DevMsg( "Request for twitch.tv account link failed: %s (code: %u, size: %u)...\n", bError ? "error" : "ok", p ? p->m_eStatusCode : 0, unBytes ); } steamapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest ); m_hHTTPRequestHandleTwitchTv = NULL; m_pLoadingAccount->m_dblTimeStampTwitchTvUpdate = Plat_FloatTime(); // push the update counter to not update for a little bit // done loading m_pLoadingAccount = NULL; } CTFStreamPanel::CTFStreamPanel( Panel *parent, const char *panelName ) : EditablePanel( parent, panelName ) { vgui::HScheme scheme = vgui::scheme()->LoadSchemeFromFileEx( enginevgui->GetPanel( PANEL_CLIENTDLL ), "resource/ClientScheme.res", "ClientScheme"); SetScheme(scheme); m_pPreviewImage = new ImagePanel( this, "PreviewImage" ); } void CTFStreamPanel::ApplySchemeSettings( IScheme *pScheme ) { BaseClass::ApplySchemeSettings( pScheme ); LoadControlSettings( "Resource/UI/StreamPanel.res" ); } void CTFStreamPanel::PerformLayout() { BaseClass::PerformLayout(); UpdatePanels(); } void CTFStreamPanel::OnCommand( const char *command ) { if ( FStrEq( command, "stream" ) ) { CStreamInfo *pInfo = GetStreamInfo(); if ( pInfo ) { vgui::system()->ShellExecute( "open", CFmtStr( "%s%s", cl_streams_mytwitchtv_channel.GetString(), pInfo->m_sDisplayName.Get() ) ); } } else { BaseClass::OnCommand( command ); } } CStreamInfo *CTFStreamPanel::GetStreamInfo() const { if ( m_strStreamInfoGlobalName.IsEmpty() ) return NULL; return g_streamManager.GetStreamInfoByName( m_strStreamInfoGlobalName.Get() ); } void CTFStreamPanel::UpdatePanels() { CStreamInfo *pInfo = GetStreamInfo(); if ( pInfo ) { SetDialogVariable( "display_name", pInfo->m_sDisplayName.Get() ); SetDialogVariable( "viewer_count", CFmtStr( "%d viewers", pInfo->m_numViewers ) ); SetDialogVariable( "text_description", pInfo->m_sTextDescription.Get() ); SetPreviewImage( pInfo->m_sPreviewImageLocalFile.Get() ); Panel* pLoadingPanel = FindChildByName( "LoadingPanel" ); if ( pLoadingPanel ) { pLoadingPanel->SetVisible( false ); } } else { SetDialogVariable( "display_name", "" ); SetDialogVariable( "viewer_count", "" ); SetDialogVariable( "text_description", "" ); SetPreviewImage( NULL ); } Panel* pStreamButton = FindChildByName( "Stream_URLButton" ); if ( pStreamButton ) { pStreamButton->SetEnabled( pInfo != NULL ); } } void CTFStreamPanel::SetPreviewImage( const char *pszPreviewImageFile ) { // clean up old image if there's one m_pPreviewImage->EvictImage(); if ( !pszPreviewImageFile ) { m_pPreviewImage->SetImage( (vgui::IImage *)0 ); return; } char szImageAbsPath[MAX_PATH]; if ( !GenerateFullPath( pszPreviewImageFile, "MOD", szImageAbsPath, ARRAYSIZE( szImageAbsPath ) ) ) { Warning( "Failed to GenerateFullPath %s\n", pszPreviewImageFile ); return; } Bitmap_t image; ConversionErrorType nErrorCode = ImgUtl_LoadBitmap( szImageAbsPath, image ); if ( nErrorCode != CE_SUCCESS ) { m_pPreviewImage->SetImage( (vgui::IImage *)0 ); return; } int wide, tall; BitmapImage *pBitmapImage = new BitmapImage; pBitmapImage->SetBitmap( image ); pBitmapImage->GetSize( wide, tall ); // The ImagePanel needs to know the scaling factor for the BitmapImage, to // center it when it's going to be rendered, and then the BitmapImage needs // to know how big it should be rendered. float flPanelWidthScale = static_cast( m_pPreviewImage->GetWide() ) / wide; m_pPreviewImage->SetShouldCenterImage( false ); m_pPreviewImage->SetShouldScaleImage( true ); m_pPreviewImage->SetScaleAmount( flPanelWidthScale ); float flSubRectWidthScale = static_cast( wide ) / image.Width(); //float flSubRectHeightScale = static_cast( tall ) / image.Height(); pBitmapImage->SetRenderSize( flSubRectWidthScale * m_pPreviewImage->GetWide(), flSubRectWidthScale * flPanelWidthScale * tall ); m_pPreviewImage->SetImage( pBitmapImage ); float flPanelHeightScale = static_cast( image.Height() ) / image.Width(); m_pPreviewImage->SetSize( m_pPreviewImage->GetWide(), flPanelHeightScale * m_pPreviewImage->GetWide() ); } CTFStreamListPanel::CTFStreamListPanel( Panel *parent, const char *panelName ) : EditablePanel( parent, panelName ) { for ( int i=0; iShellExecute( "open", "https://www.twitch.tv/directory/game/Team%20Fortress%202" ); } else { BaseClass::OnCommand( command ); } } void CTFStreamListPanel::FireGameEvent( IGameEvent *event ) { const char *pszEventName = event->GetName(); if ( FStrEq( pszEventName, "top_streams_request_finished" ) ) { // update each stream panel for ( int i=0; iSetVisible( bVisible ); if ( bVisible ) { Helper_ConfigureStreamInfoPreviewImages( g_streamManager.GetStreamInfoVec()[i], m_arrStreamPanels[i] ); } } } }