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.
808 lines
26 KiB
808 lines
26 KiB
5 years ago
|
//========= 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] );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|