diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/client/tf/tf_streams.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/client/tf/tf_streams.cpp')
| -rw-r--r-- | game/client/tf/tf_streams.cpp | 807 |
1 files changed, 807 insertions, 0 deletions
diff --git a/game/client/tf/tf_streams.cpp b/game/client/tf/tf_streams.cpp new file mode 100644 index 0000000..ea8b1fa --- /dev/null +++ b/game/client/tf/tf_streams.cpp @@ -0,0 +1,807 @@ +//========= 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 <tier0/memdbgon.h> + +// 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<CTFStreamPanel> 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<float>( m_pPreviewImage->GetWide() ) / wide; + m_pPreviewImage->SetShouldCenterImage( false ); + m_pPreviewImage->SetShouldScaleImage( true ); + m_pPreviewImage->SetScaleAmount( flPanelWidthScale ); + + float flSubRectWidthScale = static_cast<float>( wide ) / image.Width(); + //float flSubRectHeightScale = static_cast<float>( tall ) / image.Height(); + pBitmapImage->SetRenderSize( flSubRectWidthScale * m_pPreviewImage->GetWide(), flSubRectWidthScale * flPanelWidthScale * tall ); + + m_pPreviewImage->SetImage( pBitmapImage ); + + float flPanelHeightScale = static_cast<float>( 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; i<ARRAYSIZE( m_arrStreamPanels ); ++i ) + { + m_arrStreamPanels[i] = new CTFStreamPanel( this, CFmtStr( "Stream%d", i + 1 ) ); + } + + ListenForGameEvent( "top_streams_request_finished" ); +} + +void CTFStreamListPanel::ApplySchemeSettings( IScheme *pScheme ) +{ + BaseClass::ApplySchemeSettings( pScheme ); + + LoadControlSettings( "Resource/UI/StreamListPanel.res" ); +} + +void CTFStreamListPanel::OnThink() +{ + // this will fire "top_streams_request_finished" event when the job is done + g_streamManager.RequestTopStreams(); +} + +void CTFStreamListPanel::OnCommand( const char *command ) +{ + if ( FStrEq( command, "hide_streams" ) ) + { + SetVisible( false ); + } + else if ( FStrEq( command, "view_more" ) ) + { + vgui::system()->ShellExecute( "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; i<ARRAYSIZE( m_arrStreamPanels ); ++i ) + { + bool bVisible = i < g_streamManager.GetStreamInfoVec().Count(); + m_arrStreamPanels[i]->SetVisible( bVisible ); + if ( bVisible ) + { + Helper_ConfigureStreamInfoPreviewImages( g_streamManager.GetStreamInfoVec()[i], m_arrStreamPanels[i] ); + } + } + } +} |