//========== Copyright Valve Corporation, All rights reserved. ======== // // Purpose: // //============================================================================= #include "cbase.h" #include "dedicated_server_ugc_manager.h" #include "steam/isteamhttp.h" #include "ugc_utils.h" #include "tier2/fileutils.h" #include "gametypes.h" // TODO: can we swap this out based on steam universe? const char* g_szAuthKeyFilename = "webapi_authkey.txt"; const char* g_szCollectionCacheFileName = "ugc_collection_cache.txt"; const char* g_szSubscribedFilesList = "subscribed_file_ids.txt"; const char* g_szSubscribedCollectionsList = "subscribed_collection_ids.txt"; // Subdir relative to game dir to store workshop maps in const char* g_szWorkshopMapBasePath = "maps/workshop"; const char* GetApiBaseUrl( void ) { return "https://api.steampowered.com"; /* if ( steamapicontext && steamapicontext->SteamUtils() ) { if ( steamapicontext->SteamUtils()->GetConnectedUniverse() == k_EUniverseBeta ) return "https://api-beta.steampowered.com"; else if ( steamapicontext->SteamUtils()->GetConnectedUniverse() == k_EUniversePublic ) return "https://api.steampowered.com"; } Assert( 0 ); return ""; */ } ConVar sv_debug_ugc_downloads( "sv_debug_ugc_downloads", "0", FCVAR_RELEASE ); ConVar sv_broadcast_ugc_downloads( "sv_broadcast_ugc_downloads", "0", FCVAR_RELEASE ); ConVar sv_broadcast_ugc_download_progress_interval( "sv_broadcast_ugc_download_progress_interval", "8", FCVAR_RELEASE ); ConVar sv_ugc_manager_max_new_file_check_interval_secs( "sv_ugc_manager_max_new_file_check_interval_secs", "1000", FCVAR_RELEASE ); ConVar sv_remove_old_ugc_downloads( "sv_remove_old_ugc_downloads", "1", FCVAR_RELEASE ); ConVar sv_test_steam_connection_failure( "sv_test_steam_connection_failure", "0" ); CDedicatedServerWorkshopManager g_DedicatedServerWorkshopManager; CDedicatedServerWorkshopManager& DedicatedServerWorkshop( void ) { return g_DedicatedServerWorkshopManager; } CON_COMMAND( workshop_start_map, "Sets the first map to load once a workshop collection been hosted. Takes the file id of desired start map as a parameter." ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; if ( args.ArgC() != 2 ) { Msg( "Usage: workshop_start_map \n"); return; } PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] ); if ( id == 0 ) { Msg( "Invalid file id.\n"); return; } DedicatedServerWorkshop().SetTargetStartMap( id ); } CON_COMMAND( host_workshop_map, "Get the latest version of the map and host it on this server." ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; if ( args.ArgC() != 2 ) { Msg( "Usage: host_workshop_map \n"); return; } PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] ); if ( id == 0 ) { Msg( "Invalid file id.\n"); return; } // HACK: Need to load a map for steam server api to be available and download maps... peeling out the init code // would be better, but loading dust works for now. if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() ) engine->ServerCommand( CFmtStr( "map de_dust server_is_unavailable\n" ).Access() ); DedicatedServerWorkshop().HostWorkshopMap( id ); } CON_COMMAND( host_workshop_collection, "Get the latest version of maps in a workshop collection and host them as a maplist." ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; if ( args.ArgC() != 2 ) { Msg( "Usage: host_workshop_collection \n"); return; } PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] ); if ( id == 0 ) { Msg( "Invalid file id.\n"); return; } // HACK: Need to load a map for steam server api to be available and download maps... peeling out the init code // would be better, but loading dust works for now. if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() ) engine->ServerCommand( CFmtStr( "map de_dust server_is_unavailable\n" ).Access() ); DedicatedServerWorkshop().HostWorkshopMapCollection( id ); } bool DedicatedServerUGCFileInfo_t::BuildFromKV( KeyValues *pPublishedFileDetails ) { m_bIsValid = false; m_result = (EResult)pPublishedFileDetails->GetInt( "result", -1 ); // Parse file id first, we should get this even on failures. fileId = pPublishedFileDetails->GetUint64( "publishedfileid", 0ll ); if ( !fileId ) return false; if ( m_result != k_EResultOK ) return false; int appId = pPublishedFileDetails->GetInt( "consumer_appid", 0 ); if ( appId != engine->GetAppID() ) return false; if ( pPublishedFileDetails->GetInt( "banned", 0 ) != 0 ) return false; contentHandle = pPublishedFileDetails->GetUint64( "hcontent_file", 0ll ); if ( !contentHandle ) return false; const char* szUrl = pPublishedFileDetails->GetString( "file_url", NULL ); if ( !szUrl ) return false; V_strcpy_safe( m_szUrl, szUrl ); const char* szName = V_UnqualifiedFileName( pPublishedFileDetails->GetString( "filename", NULL ) ); if ( !szName ) return false; V_strcpy_safe( m_szFileName, szName ); m_unFileSizeInBytes = pPublishedFileDetails->GetInt( "file_size", 0 ); if ( !m_unFileSizeInBytes ) return false; const char* szTitle = pPublishedFileDetails->GetString( "title", NULL ); if ( !szTitle ) return false; V_strcpy_safe( m_szTitle, szTitle ); m_unTimeLastUpdated = pPublishedFileDetails->GetInt( "time_updated", 0 ); if ( !m_unTimeLastUpdated ) return false; // Assuming we're downloading maps here... V_snprintf( m_szFilePath, ARRAYSIZE(m_szFilePath), "%s/%llu/%s", g_szWorkshopMapBasePath, fileId, szName ); // TODO tags m_bIsValid = true; m_dblPlatFloatTimeReceived = Plat_FloatTime(); return true; } void ParseFileIds( const char* szFileName, CUtlVector& outVec ) { if ( filesystem->FileExists( szFileName ) ) { int fileSize; char* szFileBuf = (char*)UTIL_LoadFileForMe( szFileName, &fileSize ); if ( szFileBuf && fileSize > 0 ) { CUtlStringList fileIdList; V_SplitString( szFileBuf, "\n", fileIdList ); for ( int i = 0; i < fileIdList.Count(); ++i ) { PublishedFileId_t id = V_atoui64( fileIdList[i] ); if ( !outVec.HasElement( id ) ) outVec.AddToTail( id ); if ( sv_debug_ugc_downloads.GetBool() ) Msg( "CDedicatedServerWorkshopManager::Init: Subscribing to file id %llu\n", id ); } delete[] szFileBuf; } } } bool CDedicatedServerWorkshopManager::Init( void ) { m_UGCFileInfos.SetLessFunc( DefLessFunc( PublishedFileId_t ) ); m_mapWorkshopIdsToMapNames.SetLessFunc( DefLessFunc( PublishedFileId_t) ); m_mapPreviousCollectionQueryCache.SetLessFunc( DefLessFunc( PublishedFileId_t) ); m_fTimeLastVersionCheck = 0; GetNewestSubscribedFiles(); // HACK: So if we load a map before we hear back from steam about what files we have subscribed, // we won't submit any workshop map IDs to matchmaking. Scan for any maps in the workshop subdirectory // and use any bsps found as available maps. CUtlVector outList; RecursiveFindFilesMatchingName( &outList, g_szWorkshopMapBasePath, "*.bsp", "MOD" ); FOR_EACH_VEC( outList, i ) { CUtlString &curMap = outList[i]; PublishedFileId_t id = GetUGCMapPublishedFileID( curMap.Access() ); if ( id != 0 ) { NoteWorkshopMapOnDisk( id, curMap.Access() ); } } m_bHostedCollectionUpdatePending = false ; m_unTargetStartMap = 0; if ( g_pFullFileSystem->FileExists( g_szCollectionCacheFileName, "MOD" ) ) { KeyValues* pCollectionCacheKV = new KeyValues(""); KeyValues::AutoDelete autodelete( pCollectionCacheKV ); pCollectionCacheKV->LoadFromFile( g_pFullFileSystem, g_szCollectionCacheFileName, "MOD" ); for ( KeyValues *pDetails = pCollectionCacheKV->GetFirstSubKey(); pDetails != NULL; pDetails = pDetails->GetNextKey() ) { PublishedFileId_t collectionId = pDetails->GetUint64( "publishedfileid", 0 ); if ( collectionId != 0 ) { m_mapPreviousCollectionQueryCache.Insert( collectionId, pDetails->MakeCopy() ); } } } return true; } void CDedicatedServerWorkshopManager::LevelInitPreEntity( void ) { // reset these every level change. m_hackCurrentMapInfoCheck = 0; m_bCurrentLevelNeedsUpdate = false; m_bHostedCollectionUpdatePending = false; } void CDedicatedServerWorkshopManager::CheckIfCurrentLevelNeedsUpdate( void ) { m_bCurrentLevelNeedsUpdate = false; PublishedFileId_t id = GetUGCMapPublishedFileID( gpGlobals->mapname.ToCStr() ); if ( id != 0 ) { m_hackCurrentMapInfoCheck = id; if ( !m_FileInfoQueries.HasElement( id ) ) m_FileInfoQueries.AddToTail( id ); } } CON_COMMAND_F( ds_get_newest_subscribed_files, "Re-reads web api auth key and subscribed file lists from disk and downloads the latest updates of those files from steam", FCVAR_RELEASE ) { g_DedicatedServerWorkshopManager.GetNewestSubscribedFiles(); } void CDedicatedServerWorkshopManager::GetNewestSubscribedFiles( void ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg("CDedicatedServerWorkshopManager::GetNewestSubscribedFiles\n"); if ( engine->IsDedicatedServer() ) { Q_memset( m_szWebAPIAuthKey, 0, ARRAYSIZE( m_szWebAPIAuthKey ) ); const char *szAuthKey = CommandLine()->ParmValue( "-authkey", "" ); if ( !StringIsEmpty( szAuthKey ) ) { V_strcpy_safe( m_szWebAPIAuthKey, szAuthKey ); } else if ( filesystem->FileExists( g_szAuthKeyFilename, "MOD" ) ) { int nLength; szAuthKey = (const char *)UTIL_LoadFileForMe( g_szAuthKeyFilename, &nLength ); if ( szAuthKey != NULL ) { if ( !StringIsEmpty( szAuthKey ) ) { V_strcpy_safe( m_szWebAPIAuthKey, szAuthKey ); int len = strlen(m_szWebAPIAuthKey); while ( len > 0 && V_isspace(m_szWebAPIAuthKey[len-1]) ) { m_szWebAPIAuthKey[len-1] = 0; len--; } if (len>0) { Msg( "Loaded authkey from %s: %s\n", g_szAuthKeyFilename, m_szWebAPIAuthKey ); } } UTIL_FreeFile( (byte *)szAuthKey ); szAuthKey = NULL; } if ( StringIsEmpty(m_szWebAPIAuthKey) ) { Msg( "Auth key file %s not valid\n", g_szAuthKeyFilename ); } } if ( ! StringIsEmpty(m_szWebAPIAuthKey) ) { m_bFoundAuthKey = true; if ( sv_debug_ugc_downloads.GetBool() ) Msg( "CDedicatedServerWorkshopManager::Init: Using auth key [%s]\n", m_szWebAPIAuthKey ); } else { m_bFoundAuthKey = false; Msg( "No web api auth key specified - workshop downloads will be disabled.\n" ); } if ( m_bFoundAuthKey ) { //TODO: protect double adds? ParseFileIds( g_szSubscribedFilesList, m_FileInfoQueries ); m_vecMapsBeingUpdated.AddVectorToTail( m_FileInfoQueries ); ParseFileIds( g_szSubscribedCollectionsList, m_CollectionInfoQueries ); // If we're hosting a workshop map collection, get the latest version of those maps PublishedFileId_t id = V_atoui64( gpGlobals->mapGroupName.ToCStr() ); if ( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) && id != 0 ) { // Clumsy special case for single maps: If there's one entry and it's ID matches the collection name, it's really just a map and not a collection const CUtlStringList * pMapList = g_pGameTypes->GetMapGroupMapList( gpGlobals->mapGroupName.ToCStr() ); if ( pMapList->Count() == 1 && GetUGCMapPublishedFileID( (*pMapList)[0] ) == id ) { UpdateFile( id ); } else { m_CollectionInfoQueries.AddToTail( id ); } } m_fTimeLastVersionCheck = Plat_FloatTime(); } } } void CDedicatedServerWorkshopManager::Shutdown( void ) { KeyValues* pOutKV = new KeyValues( "CollectionInfoCache" ); KeyValues::AutoDelete autodelete( pOutKV ); FOR_EACH_MAP( m_mapPreviousCollectionQueryCache, i ) { pOutKV->AddSubKey( m_mapPreviousCollectionQueryCache[i]->MakeCopy() ); m_mapPreviousCollectionQueryCache[i]->deleteThis(); } pOutKV->SaveToFile( g_pFullFileSystem, g_szCollectionCacheFileName, "MOD" ); Cleanup(); } void CDedicatedServerWorkshopManager::Cleanup( void ) { FOR_EACH_VEC_BACK( m_PendingFileDownloads, i ) { delete m_PendingFileDownloads[i]; m_PendingFileDownloads.Remove( i ); } m_UGCFileInfos.PurgeAndDeleteElements(); m_FileInfoQueries.RemoveAll(); m_CollectionInfoQueries.RemoveAll(); m_vecWorkshopMapList.RemoveAll(); m_mapWorkshopIdsToMapNames.RemoveAll(); m_vecMapsBeingUpdated.RemoveAll(); m_bFoundAuthKey = false; m_desiredHostCollection = 0; m_bHostedCollectionUpdatePending = false; if ( m_pMapGroupBuilder ) { delete m_pMapGroupBuilder; m_pMapGroupBuilder = NULL; } } void CDedicatedServerWorkshopManager::Update( void ) { if ( !m_bFoundAuthKey ) return; if ( steamgameserverapicontext == NULL ) return; UpdatePublishedFileInfoRequests(); UpdateUGCDownloadRequests(); if ( m_pMapGroupBuilder ) { if ( m_pMapGroupBuilder->IsFinished() ) { m_pMapGroupBuilder->CreateOrUpdateMapGroup(); if ( m_desiredHostCollection != 0 && m_pMapGroupBuilder->GetFirstMap() != NULL ) { // Set the map group and changelevel if this was our target hosting map group const char* szStartMap = m_unTargetStartMap ? m_pMapGroupBuilder->GetMapMatchingId( m_unTargetStartMap ) : m_pMapGroupBuilder->GetFirstMap(); engine->ServerCommand( CFmtStr( "mapgroup %llu;map %s\n", m_pMapGroupBuilder->GetId(), szStartMap ).Access() ); m_unTargetStartMap = 0; } delete m_pMapGroupBuilder; m_pMapGroupBuilder = NULL; m_desiredHostCollection = 0; } } } bool CDedicatedServerWorkshopManager::ShouldUpdateCollection( PublishedFileId_t id, const CUtlVector& vecMaps ) { // Special case for hosted collection updates: Don't re-query all the file infos if our collection contents hasn't changed. // For large collections in locations with high ping to steam, getting all that file info takes too long and hangs up level changes. const char *szMapGroup = gpGlobals->mapGroupName.ToCStr(); bool bUpdateCollectionFiles = true; if ( m_bHostedCollectionUpdatePending && g_pGameTypes->IsWorkshopMapGroup( szMapGroup ) ) { PublishedFileId_t curHostedCollectionID = V_atoui64( szMapGroup ); Assert ( curHostedCollectionID != 0 ); // map groups hosted by dedicated servers should always have a uint64 name if ( curHostedCollectionID == id ) { const CUtlStringList &maplist = *g_pGameTypes->GetMapGroupMapList( szMapGroup ); // NOTE: this gives false positives if the collection contains invalid ids (eg, removed from workshop, etc) // because bad items in the list won't end up in the final map group, causing the count mismatch... bool bChanged = maplist.Count() != vecMaps.Count(); if ( !bChanged ) { // If count matches, make sure the contents are the same. If so, we can skip updating the collection FOR_EACH_VEC( maplist, i ) { PublishedFileId_t id = GetUGCMapPublishedFileID( maplist[i] ); if ( vecMaps.Find( id ) == -1 ) { bChanged = true; break; } } } bUpdateCollectionFiles = bChanged; } // Make sure we mark our collection as no longer updating if ( ( id == m_desiredHostCollection ) || ( id == curHostedCollectionID ) ) { m_bHostedCollectionUpdatePending = false; } } return bUpdateCollectionFiles; } void CDedicatedServerWorkshopManager::UpdatePublishedFileInfoRequests( void ) { // Process finished queries FOR_EACH_VEC_BACK( m_PendingFileInfoRequests, i ) { CPublishedFileInfoHTTPRequest* pCurRequest = m_PendingFileInfoRequests[i]; if( !pCurRequest->IsFinished() ) // still waiting on a response continue; if ( pCurRequest->GetLastHTTPResult() != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() ) { // Handle http errors, retries // Remove failed map ids from the pending list if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Failed to get file info information from steam, HTTP status: %d\n. Missing info for file ids: ", pCurRequest->GetLastHTTPResult() ); FOR_EACH_VEC( pCurRequest->GetItemsQueried(), i ) { PublishedFileId_t id = pCurRequest->GetItemsQueried()[i]; OnFileInfoRequestFailed( id ); if ( sv_debug_ugc_downloads.GetBool() ) Msg( "%llu ", id ); } if ( sv_debug_ugc_downloads.GetBool() ) Msg( "\n" ); } else { FOR_EACH_VEC( pCurRequest->GetFileInfoList(), j ) { const DedicatedServerUGCFileInfo_t* pCurInfo = pCurRequest->GetFileInfoList()[j]; OnFileInfoReceived( pCurInfo ); if ( pCurInfo->m_result == k_EResultOK && pCurInfo->m_bIsValid ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "CDedicatedServerWorkshopManager: received file details for id %llu: '%s'.\n", pCurInfo->fileId, pCurInfo->m_szTitle ? pCurInfo->m_szTitle : "" ); RemoveFileInfo( pCurInfo->fileId ); // Clear any existing entry and cancel any pending download. DedicatedServerUGCFileInfo_t* pNewFileInfo = new DedicatedServerUGCFileInfo_t; V_memcpy( (void*)pNewFileInfo, (void*)pCurInfo, sizeof ( DedicatedServerUGCFileInfo_t ) ); m_UGCFileInfos.Insert( pNewFileInfo->fileId, pNewFileInfo ); // Skip downloading if this is an 'info only' id. if ( m_hackCurrentMapInfoCheck == pNewFileInfo->fileId ) { m_hackCurrentMapInfoCheck = 0; continue; } if ( !IsFileLatestVersion( pNewFileInfo ) ) { QueueDownloadFile( pNewFileInfo ); } else { OnFileDownloaded( pNewFileInfo ); if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Skipping download for file id %llu:'%s' - version on disk is latest.\n", pNewFileInfo->fileId, pNewFileInfo->m_szTitle ? pNewFileInfo->m_szTitle : "" ); } } else { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Failed to parse file details KV for id %llu. Result enum: %d\n", pCurInfo->fileId, pCurInfo->m_result ); if ( pCurInfo->m_result == k_EResultFileNotFound ) Msg( "File id %llu not found. Probably removed from workshop\n", pCurInfo->fileId ); } } } delete pCurRequest; // request dealt with m_PendingFileInfoRequests.Remove( i ); } FOR_EACH_VEC_BACK( m_PendingCollectionInfoRequests, i ) { CCollectionInfoHTTPRequest *pCurRequest = m_PendingCollectionInfoRequests[i]; if ( !pCurRequest->IsFinished() ) continue; if ( pCurRequest->GetLastHTTPResult() != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Failed to get file info information from steam, HTTP status: %d\n. Missing info for collection ids: ", pCurRequest->GetLastHTTPResult() ); FOR_EACH_VEC( pCurRequest->GetItemsQueried(), i ) { PublishedFileId_t id = pCurRequest->GetItemsQueried()[i]; OnCollectionInfoRequestFailed( id ); if ( sv_debug_ugc_downloads.GetBool() ) Msg( "%llu ", id ); if ( id == m_desiredHostCollection ) { int idx = m_mapPreviousCollectionQueryCache.Find( id ); if ( idx != m_mapPreviousCollectionQueryCache.InvalidIndex() ) { ParseCollectionInfo( m_mapPreviousCollectionQueryCache[idx] ); } else { m_bHostedCollectionUpdatePending = false; // failed to get info on host collection, and we didn't have it in our cache } } } if ( sv_debug_ugc_downloads.GetBool() ) Msg( "\n" ); } else { KeyValues* pCollectionDetails = pCurRequest->GetResponseKV(); for ( KeyValues *pDetails = pCollectionDetails->GetFirstSubKey(); pDetails != NULL; pDetails = pDetails->GetNextKey() ) { PublishedFileId_t collectionId = ParseCollectionInfo( pDetails ); if ( collectionId != 0 ) { // Save previously queried collection infos to disk in case we lose connection to steam int idx = m_mapPreviousCollectionQueryCache.Find( collectionId ); if ( idx == m_mapPreviousCollectionQueryCache.InvalidIndex() ) idx = m_mapPreviousCollectionQueryCache.Insert( collectionId ); else m_mapPreviousCollectionQueryCache[idx]->deleteThis(); m_mapPreviousCollectionQueryCache[idx] = pDetails->MakeCopy(); } } } delete pCurRequest; m_PendingCollectionInfoRequests.Remove( i ); } if ( m_FileInfoQueries.Count() > 0 ) { CPublishedFileInfoHTTPRequest *pRequest = new CPublishedFileInfoHTTPRequest( m_FileInfoQueries ); pRequest->CreateHTTPRequest( m_szWebAPIAuthKey ); m_PendingFileInfoRequests.AddToTail( pRequest ); m_FileInfoQueries.RemoveAll(); } if ( m_CollectionInfoQueries.Count() > 0 ) { CCollectionInfoHTTPRequest *pRequest = new CCollectionInfoHTTPRequest( m_CollectionInfoQueries ); pRequest->CreateHTTPRequest( m_szWebAPIAuthKey ); m_PendingCollectionInfoRequests.AddToTail( pRequest ); m_CollectionInfoQueries.RemoveAll(); } } void CDedicatedServerWorkshopManager::UpdateUGCDownloadRequests( void ) { if ( m_PendingFileDownloads.Count() ) { // TODO: Handle timeouts/errors? m_PendingFileDownloads[0]->Update(); if ( m_PendingFileDownloads[0]->IsFinished() ) { OnFileDownloaded( m_PendingFileDownloads[0]->GetFileInfo() ); delete m_PendingFileDownloads[0]; m_PendingFileDownloads.Remove( 0 ); } } /* BUG/TODO: Downloading lots of files at the same time runs out of memory in GetHTTPResponseBodyData growing a buffer... Only downloading one at a time for now. FOR_EACH_VEC_BACK( m_PendingFileDownloads, i ) { m_PendingFileDownloads[i]->Update(); if ( m_PendingFileDownloads[i]->IsFinished() ) { delete m_PendingFileDownloads[i]; m_PendingFileDownloads.Remove( i ); } } */ } void CDedicatedServerWorkshopManager::QueueDownloadFile( const DedicatedServerUGCFileInfo_t *pFileInfo ) { CStreamingUGCDownloader *pDownloader = new CStreamingUGCDownloader; pDownloader->StartFileDownload( pFileInfo, 1024*1024 ); m_PendingFileDownloads.AddToTail( pDownloader ); } bool CDedicatedServerWorkshopManager::IsFileLatestVersion( const DedicatedServerUGCFileInfo_t* ugcInfo ) { // never try to update an official map, they're shipped with the depot if ( UGCUtil_IsOfficialMap( ugcInfo->fileId ) ) return true; if ( g_pFullFileSystem->FileExists( ugcInfo->m_szFilePath ) ) { // mtime needs to match the time last updated exactly, as we slam the file time when we download // so an earlier time is out of date and a later time may be modified due to file copying. uint32 fileTime = (uint32)g_pFullFileSystem->GetFileTime( ugcInfo->m_szFilePath ); if ( ugcInfo->m_unTimeLastUpdated == fileTime ) return true; } return false; } void CDedicatedServerWorkshopManager::RemoveFileInfo( PublishedFileId_t id ) { unsigned short idx = m_UGCFileInfos.Find( id ); if ( idx != m_UGCFileInfos.InvalidIndex() ) { // Cancel any pending download FOR_EACH_VEC_BACK( m_PendingFileDownloads, i ) { if ( m_PendingFileDownloads[i]->GetPublishedFileId() == id ) { delete m_PendingFileDownloads[i]; m_PendingFileDownloads.Remove( i ); } } delete m_UGCFileInfos[idx]; m_UGCFileInfos.RemoveAt( idx ); } } bool CDedicatedServerWorkshopManager::GetMapsMatchingName( const char* szMapName, CUtlVector& outVec ) const { bool bFoundAny = false; FOR_EACH_MAP( m_UGCFileInfos, i ) { const DedicatedServerUGCFileInfo_t *pInfo = m_UGCFileInfos[i]; // compare just map name (ignore extension and paths passed in) char szBaseQueryMapName[MAX_PATH], szBaseUGCMapName[MAX_PATH]; V_FileBase( szMapName, szBaseQueryMapName, ARRAYSIZE( szBaseQueryMapName ) ); V_FileBase( pInfo->m_szFileName, szBaseUGCMapName, ARRAYSIZE( szBaseUGCMapName ) ); if ( V_strcmp( szBaseQueryMapName, szBaseUGCMapName ) == 0 ) { outVec.AddToTail( pInfo ); bFoundAny = true; } } return bFoundAny; } // Get the maps for which we downloaded UGC information successfully void CDedicatedServerWorkshopManager::GetWorkshopMasWithValidUgcInformation( CUtlVector& outVec ) const { FOR_EACH_MAP( m_UGCFileInfos, i ) { const DedicatedServerUGCFileInfo_t *pInfo = m_UGCFileInfos[i]; if ( !pInfo ) continue; outVec.AddToTail( pInfo ); } } // HACK: Using the map's directory to get the published file id... PublishedFileId_t CDedicatedServerWorkshopManager::GetUGCMapPublishedFileID( const char* szPathToUGCMap ) const { char tmp[MAX_PATH]; V_strcpy_safe( tmp, szPathToUGCMap ); V_FixSlashes( tmp, '/' ); // internal path strings use forward slashes, make sure we compare like that. //if ( !V_strncmp( tmp, g_szWorkshopMapBasePath, strlen( g_szWorkshopMapBasePath ) ) ) if ( V_strstr( tmp, "workshop/" ) ) { V_StripFilename(tmp); V_StripTrailingSlash(tmp); const char* szDirName = V_GetFileName(tmp); return (PublishedFileId_t)V_atoui64(szDirName); } /* char szBspNoExtension[MAX_PATH]; V_strcpy_safe( szBspNoExtension, szPathToUGCMap ); V_StripExtension( szBspNoExtension, szBspNoExtension, sizeof( szBspNoExtension ) ); char szElemPathNoExt[MAX_PATH]; FOR_EACH_MAP( m_UGCFileInfos, i ) { DedicatedServerUGCFileInfo_t* pElem = m_UGCFileInfos.Element(i); if ( !V_stricmp( szPathToUGCMap, szBSPPath ) ) { return m_UGCFileInfos.Key(i); } } */ return 0; } const CUtlVector< PublishedFileId_t > & CDedicatedServerWorkshopManager::GetWorkshopMapList( void ) const { return m_vecWorkshopMapList; } // Records all workshop maps we have on disk (may or may not be up to date). void CDedicatedServerWorkshopManager::NoteWorkshopMapOnDisk( PublishedFileId_t id, const char* szPath ) { if ( m_vecWorkshopMapList.Find( id ) == m_vecWorkshopMapList.InvalidIndex() ) { m_vecWorkshopMapList.AddToTail( id ); int idx = m_mapWorkshopIdsToMapNames.Insert( id ); m_mapWorkshopIdsToMapNames[idx].Set( szPath ); V_FixSlashes( m_mapWorkshopIdsToMapNames[idx].Get(), '/' ); // Always refer to map paths with forward slashes internally for consistancy. } } void CDedicatedServerWorkshopManager::HostWorkshopMap( PublishedFileId_t id ) { UpdateFile( id ); m_desiredHostCollection = id; } void CDedicatedServerWorkshopManager::HostWorkshopMapCollection( PublishedFileId_t id ) { if ( m_bFoundAuthKey == false ) { Warning( "host_workshop_collection: Web API auth key not found!\n" ); } if ( !m_CollectionInfoQueries.HasElement( id ) ) m_CollectionInfoQueries.AddToTail( id ); m_desiredHostCollection = id; } // Called each time we get ugc file metadata from steam. Assumes this will get called before OnFileDownloaded. void CDedicatedServerWorkshopManager::OnFileInfoReceived( const DedicatedServerUGCFileInfo_t *pInfo ) { if ( pInfo->m_result != k_EResultOK ) { if ( m_pMapGroupBuilder ) { m_pMapGroupBuilder->RemoveRequiredMap( pInfo->fileId ); } m_vecMapsBeingUpdated.FindAndFastRemove( pInfo->fileId ); } // Host single maps as a collection of one if ( pInfo->fileId == m_desiredHostCollection ) { if ( m_pMapGroupBuilder ) delete m_pMapGroupBuilder; CUtlVector< PublishedFileId_t > vecMapFile; vecMapFile.AddToTail( pInfo->fileId ); m_pMapGroupBuilder = new CWorkshopMapGroupBuilder( pInfo->fileId, vecMapFile ); } if( pInfo->fileId == m_hackCurrentMapInfoCheck ) { m_bCurrentLevelNeedsUpdate = !IsFileLatestVersion( pInfo ); } } void CDedicatedServerWorkshopManager::OnFileInfoRequestFailed( PublishedFileId_t id ) { m_vecMapsBeingUpdated.FindAndRemove( id ); // no longer being updated if ( m_pMapGroupBuilder ) { m_pMapGroupBuilder->RemoveRequiredMap( id ); } } void CDedicatedServerWorkshopManager::OnCollectionInfoReceived( PublishedFileId_t collectionId, const CUtlVector< PublishedFileId_t > & vecCollectionItems ) { if ( vecCollectionItems.Count() > 0 ) { PublishedFileId_t curHostedCollection = 0; if( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) ) { curHostedCollection = V_atoui64( gpGlobals->mapGroupName.ToCStr() ); } // Make/refresh a mapgroup if it's the one we want to/are hosting. if ( collectionId == m_desiredHostCollection || collectionId == curHostedCollection ) { if ( m_pMapGroupBuilder ) delete m_pMapGroupBuilder; m_pMapGroupBuilder = new CWorkshopMapGroupBuilder( collectionId, vecCollectionItems ); m_bHostedCollectionUpdatePending = false; } } } void CDedicatedServerWorkshopManager::OnCollectionInfoRequestFailed( PublishedFileId_t id ) { } void CDedicatedServerWorkshopManager::OnFileDownloaded( const DedicatedServerUGCFileInfo_t *pInfo ) { if ( m_pMapGroupBuilder ) { m_pMapGroupBuilder->OnMapDownloaded( pInfo ); } m_vecMapsBeingUpdated.FindAndFastRemove( pInfo->fileId ); NoteWorkshopMapOnDisk( pInfo->fileId, pInfo->m_szFilePath ); } bool CDedicatedServerWorkshopManager::HasPendingMapDownloads( void ) const { if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() || !steamgameserverapicontext->SteamGameServer() || !steamgameserverapicontext->SteamGameServer()->BLoggedOn() || (engine->GetGameServerSteamID() && engine->GetGameServerSteamID()->GetEAccountType() == k_EAccountTypeInvalid) ) return false; // TODO: Timeouts, errors // If we have maps in the list waiting on info/downloads, or if we are trying to get the lastest info on our hosted collection return ( m_vecMapsBeingUpdated.Count() != 0 || m_bHostedCollectionUpdatePending ); } void CDedicatedServerWorkshopManager::UpdateFile( PublishedFileId_t id ) { if ( !m_FileInfoQueries.HasElement( id ) ) m_FileInfoQueries.AddToTail( id ); if ( !m_vecMapsBeingUpdated.HasElement( id ) ) m_vecMapsBeingUpdated.AddToTail( id ); } void CDedicatedServerWorkshopManager::UpdateFiles( const CUtlVector& vecFileIDs ) { FOR_EACH_VEC( vecFileIDs, i ) { UpdateFile( vecFileIDs[i] ); } } bool CDedicatedServerWorkshopManager::CurrentLevelNeedsUpdate( void ) const { return m_bCurrentLevelNeedsUpdate; } void CDedicatedServerWorkshopManager::CheckForNewVersion( PublishedFileId_t id ) { bool bUpdateWorkshopCollectionToo = true; if ( m_fTimeLastVersionCheck != 0 && ( Plat_FloatTime() - m_fTimeLastVersionCheck < sv_ugc_manager_max_new_file_check_interval_secs.GetFloat() ) ) { // If we don't have the map then we have to actually go and download it regardless of the timeout MapFileIdToUgcFileInfo_t::IndexType_t idxFileInfo = m_UGCFileInfos.Find( id ); if ( ( idxFileInfo != m_UGCFileInfos.InvalidIndex() ) && m_UGCFileInfos.Element( idxFileInfo ) ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Skipping new version check for file id %llu, next check in %.2f seconds\n", id, sv_ugc_manager_max_new_file_check_interval_secs.GetFloat() - (Plat_FloatTime() - m_fTimeLastVersionCheck) ); return; } // We have recently checked the contents of workshop collection, // this time we are downloading some other map that users want // to play, so don't check collection bUpdateWorkshopCollectionToo = false; } UpdateFile( id ); if ( !bUpdateWorkshopCollectionToo ) return; // Remember last time we did full update m_fTimeLastVersionCheck = Plat_FloatTime(); // check if the map collection changed if ( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) ) { PublishedFileId_t hostedCollectionID = V_atoui64( gpGlobals->mapGroupName.ToCStr() ); // If collection's id is the map's id, then this is a mapgroup of one... no need to check for collection changes. if ( hostedCollectionID != id ) { if ( !m_CollectionInfoQueries.HasElement( hostedCollectionID ) ) m_CollectionInfoQueries.AddToTail( hostedCollectionID ); m_bHostedCollectionUpdatePending = true; } } } const char* CDedicatedServerWorkshopManager::GetUGCMapPath( PublishedFileId_t id ) const { int idx = m_mapWorkshopIdsToMapNames.Find( id ); if ( idx != m_mapWorkshopIdsToMapNames.InvalidIndex() ) { return m_mapWorkshopIdsToMapNames[idx]; } else { return NULL; } } PublishedFileId_t CDedicatedServerWorkshopManager::ParseCollectionInfo( KeyValues * pDetails ) { int collection_detail_result = pDetails->GetInt( "result", 0 ); EResult hResult = (EResult)collection_detail_result; KeyValues *pChildren = pDetails->FindKey( "children" ); PublishedFileId_t ret = 0; if ( hResult == k_EResultOK && pChildren ) { PublishedFileId_t collectionId = pDetails->GetUint64( "publishedfileid", 0 ); if ( sv_debug_ugc_downloads.GetBool() ) { Msg( "Received info for collection id %llu:\n", collectionId ); } CUtlVector vecCollectionIDs; for ( KeyValues *pFile = pChildren->GetFirstSubKey(); pFile != NULL; pFile = pFile->GetNextKey() ) { PublishedFileId_t id = pFile->GetUint64( "publishedfileid" ); vecCollectionIDs.AddToTail( id ); if ( sv_debug_ugc_downloads.GetBool() ) Msg( " file ID: %llu\n", id ); } if ( ShouldUpdateCollection( collectionId, vecCollectionIDs ) ) { UpdateFiles( vecCollectionIDs ); OnCollectionInfoReceived( collectionId, vecCollectionIDs ); } ret = collectionId; } return ret; } CStreamingUGCDownloader::CStreamingUGCDownloader():m_fileBuffer( 1024*1024, 1024*1024, 0 ) { m_ioAsyncControl = NULL; m_unChunkSize = 0; m_unBytesReceived = 0; m_unFileSizeInBytes = 0; m_pFileInfo = NULL; m_bIsFinished = false; m_bHTTPRequestPending = false; m_flTimeLastMessage = 0.0f; } void CStreamingUGCDownloader::Cleanup( void ) { if ( m_ioAsyncControl ) { filesystem->AsyncAbort( m_ioAsyncControl ); filesystem->AsyncFinish( m_ioAsyncControl, true ); filesystem->AsyncRelease( m_ioAsyncControl ); m_ioAsyncControl = NULL; } m_fileBuffer.Clear(); if ( filesystem->FileExists( m_szTempFileName ) ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Clearing temp file(%s) for %llu : %s\n", m_szTempFileName, m_pFileInfo->fileId, m_pFileInfo->m_szFileName ); filesystem->RemoveFile( m_szTempFileName ); } if ( steamgameserverapicontext ) { ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); if ( pHTTP && m_bHTTPRequestPending ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Canceling download for %llu : %s\n", m_pFileInfo->fileId, m_pFileInfo->m_szFileName ); pHTTP->ReleaseHTTPRequest( m_hReq ); } } m_httpRequestCallback.Cancel(); } CStreamingUGCDownloader::~CStreamingUGCDownloader() { Cleanup(); } void CStreamingUGCDownloader::StartFileDownload( const DedicatedServerUGCFileInfo_t *pFileInfo, uint32 unChunkSize ) { m_bIsFinished = false; V_snprintf( m_szTempFileName, ARRAYSIZE( m_szTempFileName ), "%s/%llu/%llu.tmp", g_szWorkshopMapBasePath, pFileInfo->fileId, pFileInfo->fileId ); // Make sure target directory exists char buf[MAX_PATH]; V_ExtractFilePath( m_szTempFileName, buf, sizeof( buf ) ); g_pFullFileSystem->CreateDirHierarchy( buf, "DEFAULT_WRITE_PATH" ); if ( filesystem->FileExists( m_szTempFileName, "MOD" ) ) { filesystem->RemoveFile(m_szTempFileName, "MOD" ); } m_pFileInfo = pFileInfo; m_unBytesReceived = 0; m_unFileSizeInBytes = pFileInfo->m_unFileSizeInBytes; m_unChunkSize = unChunkSize; m_fileBuffer.EnsureCapacity( m_unChunkSize ); // Doing one download at a time-- don't start requesting content until it's this downloader's turn. // HTTPRequestPartialContent( 0, unChunkSize ); V_strcpy_safe( m_szMapTitle, pFileInfo->m_szTitle ); if ( sv_debug_ugc_downloads.GetBool() ) { Msg( "Starting download for file id %llu:'%s'.\n", pFileInfo->fileId, pFileInfo->m_szTitle ? pFileInfo->m_szTitle : "" ); } if ( sv_broadcast_ugc_downloads.GetBool() ) { UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "Server: Downloading new map '%s', please wait...", pFileInfo->m_szTitle ) ); m_flTimeLastMessage = gpGlobals->curtime; } } void CStreamingUGCDownloader::HTTPRequestPartialContent( uint32 rangeStart, uint32 rangeEnd ) { Assert( steamgameserverapicontext ); if ( steamgameserverapicontext == NULL ) return; ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); Assert( pHTTP ); if ( !pHTTP ) return; m_hReq = pHTTP->CreateHTTPRequest( k_EHTTPMethodGET, m_pFileInfo->m_szUrl ); SteamAPICall_t hCall; CFmtStr byteRange( "bytes=%d-%d", rangeStart, rangeEnd ); pHTTP->SetHTTPRequestHeaderValue( m_hReq, "range", byteRange.Access() ); pHTTP->SendHTTPRequest( m_hReq, &hCall ); m_httpRequestCallback.SetGameserverFlag(); m_httpRequestCallback.Set( hCall, this, &CStreamingUGCDownloader::OnHTTPRequestComplete ); m_bHTTPRequestPending = true; if ( sv_broadcast_ugc_downloads.GetBool() && gpGlobals->curtime - m_flTimeLastMessage > sv_broadcast_ugc_download_progress_interval.GetFloat() ) { UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "Server: %.0f%% downloaded for '%s'...", ((float)rangeStart / (float)m_unFileSizeInBytes) * 100.0f, m_szMapTitle ) ); m_flTimeLastMessage = gpGlobals->curtime; } } void CStreamingUGCDownloader::OnHTTPRequestComplete( HTTPRequestCompleted_t *arg, bool bFailed ) { Assert( steamgameserverapicontext ); if ( steamgameserverapicontext == NULL ) return; ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); Assert( pHTTP ); if ( !pHTTP ) return; Assert( arg ); if ( !arg ) return; if ( arg->m_eStatusCode == k_EHTTPStatusCode206PartialContent ) { uint32 unBodySize; if ( pHTTP->GetHTTPResponseBodySize( arg->m_hRequest, &unBodySize ) ) { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Receiving bytes %u-%u for file %s (%s)\n", m_unBytesReceived, m_unBytesReceived + unBodySize, m_szTempFileName, m_pFileInfo->m_szFileName ); m_unBytesReceived += unBodySize; m_fileBuffer.EnsureCapacity( unBodySize ); m_fileBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, unBodySize ); if ( pHTTP->GetHTTPResponseBodyData( arg->m_hRequest, (uint8*)m_fileBuffer.Base(), unBodySize ) ) { filesystem->AsyncAppend( m_szTempFileName, (void*)m_fileBuffer.Base(), unBodySize, false, &m_ioAsyncControl ); } } // todo FAIL-- abort } pHTTP->ReleaseHTTPRequest( arg->m_hRequest ); m_bHTTPRequestPending = false; } void CStreamingUGCDownloader::Update( void ) { Assert( steamgameserverapicontext ); if ( steamgameserverapicontext == NULL ) return; ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); Assert( pHTTP ); if ( !pHTTP ) return; // Free to ask for more content if async write is done, or if we haven't started writing yet. bool bDoneWriting = m_ioAsyncControl == NULL || filesystem->AsyncStatus( m_ioAsyncControl ) == FSASYNC_OK; if ( bDoneWriting == true && m_bHTTPRequestPending == false ) { if ( m_unBytesReceived < m_unFileSizeInBytes ) { HTTPRequestPartialContent( m_unBytesReceived, MIN( m_unBytesReceived + m_unChunkSize, m_unFileSizeInBytes ) - 1 ); } else { // remove the older file if it exists //BUG: If we're running this map, the copy will fail. Defer in that case. if ( filesystem->FileExists( m_pFileInfo->m_szFilePath ) ) { filesystem->RemoveFile( m_pFileInfo->m_szFilePath ); } // If authors rename the map file, old versions get orphaned in the workshop directory. Nuke any bsp here. if ( sv_remove_old_ugc_downloads.GetBool() ) { CUtlVector outList; AddFilesToList( outList, CFmtStr( "%s/%llu/", g_szWorkshopMapBasePath, m_pFileInfo->fileId ).Access(), "MOD", "bsp" ); FOR_EACH_VEC( outList, i ) { filesystem->RemoveFile( outList[i] ); } } char szFullPathToTempFile[MAX_PATH]; g_pFullFileSystem->RelativePathToFullPath( m_szTempFileName, "MOD", szFullPathToTempFile, sizeof( szFullPathToTempFile ) ); if ( UnzipFile( szFullPathToTempFile ) == false ) { // Not a zip file, just rename it g_pFullFileSystem->RenameFile( m_szTempFileName, m_pFileInfo->m_szFilePath ); } // // Timestamp the file to match workshop updated timestamp // UGCUtil_TimestampFile( m_pFileInfo->m_szFilePath, m_pFileInfo->m_unTimeLastUpdated ); m_bIsFinished = true; if ( 1 )//sv_debug_ugc_downloads.GetBool() ) { Msg( "Download finished for %llu:'%s'. Moving %s to %s.\n", m_pFileInfo->fileId, m_pFileInfo->m_szTitle ? m_pFileInfo->m_szTitle : "", m_szTempFileName, m_pFileInfo->m_szFilePath ); } } } } void CWorkshopMapGroupBuilder::OnMapDownloaded( const DedicatedServerUGCFileInfo_t *pInfo ) { MapOnDisk( pInfo->fileId, pInfo->m_szFilePath ); } CWorkshopMapGroupBuilder::CWorkshopMapGroupBuilder( PublishedFileId_t id, const CUtlVector< PublishedFileId_t >& mapFileIDs ) { m_id = id; m_pendingMapInfos.AddVectorToTail( mapFileIDs ); FOR_EACH_VEC( m_pendingMapInfos, i ) { const char* szPath = DedicatedServerWorkshop().GetUGCMapPath( m_pendingMapInfos[i] ); if ( szPath ) { MapOnDisk( m_pendingMapInfos[i], szPath ); } } } void CWorkshopMapGroupBuilder::CreateOrUpdateMapGroup( void ) { g_pGameTypes->CreateOrUpdateWorkshopMapGroup( CFmtStr( "%llu", m_id ).Access(), m_Maps ); } const char* CWorkshopMapGroupBuilder::GetFirstMap( void ) const { return m_Maps.Count() > 0 ? m_Maps.Head() : NULL; } const char* CWorkshopMapGroupBuilder::GetMapMatchingId( PublishedFileId_t id ) const { FOR_EACH_VEC( m_Maps, i ) { if ( id == GetMapIDFromMapPath( m_Maps[i] ) ) return m_Maps[i]; } return GetFirstMap(); } void CWorkshopMapGroupBuilder::RemoveRequiredMap( PublishedFileId_t id ) { m_pendingMapInfos.FindAndFastRemove( id ); } void CWorkshopMapGroupBuilder::MapOnDisk( PublishedFileId_t id, const char* szPath ) { int idx = m_pendingMapInfos.Find( id ); if ( idx != m_pendingMapInfos.InvalidIndex() ) { m_pendingMapInfos.FastRemove( idx ); // Build path to the map file without any extensions and without the 'maps' dir in the path (maps dir is assumed by other systems). char szMapPath[MAX_PATH]; char szInputPath[MAX_PATH]; V_strcpy_safe( szInputPath, szPath ); V_FixSlashes( szInputPath, '/' ); const char* szMapsPrefix = "maps/"; if ( V_stristr( szInputPath, szMapsPrefix ) == szInputPath ) { V_strcpy_safe( szMapPath, szInputPath + strlen( szMapsPrefix ) ); } V_StripExtension( szMapPath, szMapPath, sizeof( szMapPath ) ); m_Maps.CopyAndAddToTail( szMapPath ); // CUtlStringList auto purges on destruct } } CBaseWorkshopHTTPRequest::CBaseWorkshopHTTPRequest( const CUtlVector &vecFileIDs ) { m_handle = INVALID_HTTPREQUEST_HANDLE; m_bFinished = false; m_vecItemsQueried.AddVectorToTail( vecFileIDs ); } CBaseWorkshopHTTPRequest::~CBaseWorkshopHTTPRequest() { if ( steamgameserverapicontext ) { ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); if ( pHTTP && m_handle != INVALID_HTTPREQUEST_HANDLE ) { pHTTP->ReleaseHTTPRequest( m_handle ); } } m_httpCallback.Cancel(); } void CBaseWorkshopHTTPRequest::OnHTTPRequestComplete( HTTPRequestCompleted_t *arg, bool bFailed ) { m_bFinished = true; m_lastHTTPResult = arg->m_eStatusCode; ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); if ( arg->m_bRequestSuccessful == false ) { Warning( "Server UGC Manager: Failed to get file info. Internal IHTTP error or clientside internet connection problem." ); } else if ( arg->m_eStatusCode != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() ) { Warning( "Server UGC Manager: Failed to get file info. HTTP status %d \n", arg->m_eStatusCode ); } else { uint32 unBodySize; if ( !pHTTP->GetHTTPResponseBodySize( arg->m_hRequest, &unBodySize ) ) { Assert( 0 ); Warning( "Server UGC Manager: GetHTTPResponseBodySize failed\n" ); } else { if ( sv_debug_ugc_downloads.GetBool() ) Msg( "Fetched %d bytes via HTTP:\n", unBodySize ); if ( unBodySize > 0 ) { CUtlBuffer resBuffer( 0, unBodySize, 0 ); resBuffer.SetBufferType( true, true ); resBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, unBodySize ); pHTTP->GetHTTPResponseBodyData( arg->m_hRequest, (uint8*)resBuffer.Base(), resBuffer.TellPut() ); KeyValues *pResponseKV = new KeyValues(""); pResponseKV->UsesEscapeSequences( true ); KeyValuesAD autodelete( pResponseKV ); bool bLoadSucessful = pResponseKV->LoadFromBuffer( NULL, resBuffer ); if ( sv_debug_ugc_downloads.GetBool() ) KeyValuesDumpAsDevMsg( pResponseKV, 1 ); if ( !bLoadSucessful ) Msg( "CDedicatedServerWorkshopManager: Failed to load http result to KV buffer\n" ); if ( bLoadSucessful ) { ProcessHTTPResponse( pResponseKV ); } } } } pHTTP->ReleaseHTTPRequest( arg->m_hRequest ); } CPublishedFileInfoHTTPRequest::CPublishedFileInfoHTTPRequest( const CUtlVector& vecFileIDs ) : CBaseWorkshopHTTPRequest( vecFileIDs ) { } CPublishedFileInfoHTTPRequest::~CPublishedFileInfoHTTPRequest() { m_vecFileInfos.PurgeAndDeleteElements(); } HTTPRequestHandle CPublishedFileInfoHTTPRequest::CreateHTTPRequest( const char* szAuthKey ) { if ( steamgameserverapicontext && steamgameserverapicontext->SteamHTTP() ) { CFmtStr strItemCount( "%d", m_vecItemsQueried.Count() ); const char* szUrl = CFmtStr("%s/Service/PublishedFile/GetDetails/v1/", GetApiBaseUrl()).Access(); ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); m_handle = pHTTP->CreateHTTPRequest( k_EHTTPMethodGET, szUrl ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "format", "vdf" ); FOR_EACH_VEC( m_vecItemsQueried, i ) { CFmtStr entry( "publishedfileids[%d]", i ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, entry.Access(), CFmtStr("%llu", m_vecItemsQueried[i] ).Access() ); } pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "key", szAuthKey ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "minimal_details", "1" ); SteamAPICall_t hCall; pHTTP->SendHTTPRequest( m_handle, &hCall); m_httpCallback.SetGameserverFlag(); m_httpCallback.Set( hCall, this, &CBaseWorkshopHTTPRequest::OnHTTPRequestComplete ); } else { m_bFinished = true; } return m_handle; } void CPublishedFileInfoHTTPRequest::ProcessHTTPResponse( KeyValues *pResponseKV ) { KeyValues *pPublishedFileDetails = pResponseKV->FindKey( "publishedfiledetails", false ); if ( pPublishedFileDetails ) { for ( KeyValues *fileDetails = pPublishedFileDetails->GetFirstSubKey(); fileDetails != NULL; fileDetails = fileDetails->GetNextKey() ) { DedicatedServerUGCFileInfo_t * pNewFileInfo = new DedicatedServerUGCFileInfo_t; pNewFileInfo->BuildFromKV( fileDetails ); m_vecFileInfos.AddToTail( pNewFileInfo ); } } } CCollectionInfoHTTPRequest::CCollectionInfoHTTPRequest( const CUtlVector& vecFileIDs ) : CBaseWorkshopHTTPRequest( vecFileIDs ) { m_pResponseKV = NULL; } CCollectionInfoHTTPRequest::~CCollectionInfoHTTPRequest() { if ( m_pResponseKV ) { m_pResponseKV->deleteThis(); m_pResponseKV = NULL; } } HTTPRequestHandle CCollectionInfoHTTPRequest::CreateHTTPRequest( const char* szAuthKey /*= NULL */ ) { if ( steamgameserverapicontext && steamgameserverapicontext->SteamHTTP() ) { CFmtStr strItemCount( "%d", m_vecItemsQueried.Count() ); const char* szUrl = CFmtStr("%s/ISteamRemoteStorage/GetCollectionDetails/v0001/", GetApiBaseUrl()).Access(); ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP(); m_handle = pHTTP->CreateHTTPRequest( k_EHTTPMethodPOST, szUrl ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "format", "vdf" ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "collectioncount", strItemCount.Access() ); FOR_EACH_VEC( m_vecItemsQueried, i ) { CFmtStr entry( "publishedfileids[%d]", i ); pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, entry.Access(), CFmtStr("%llu", m_vecItemsQueried[i] ).Access() ); } pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "key", szAuthKey ); SteamAPICall_t hCall; pHTTP->SendHTTPRequest( m_handle, &hCall); m_httpCallback.SetGameserverFlag(); m_httpCallback.Set( hCall, this, &CBaseWorkshopHTTPRequest::OnHTTPRequestComplete ); } else { m_bFinished = true; } return m_handle; } void CCollectionInfoHTTPRequest::ProcessHTTPResponse( KeyValues *pResponseKV ) { KeyValues *pCollectionDetails = pResponseKV->FindKey( "collectiondetails" ); if ( pCollectionDetails ) { Assert( m_pResponseKV == NULL ); if ( m_pResponseKV ) m_pResponseKV->deleteThis(); m_pResponseKV = pCollectionDetails->MakeCopy(); } else { Assert( 0 ); Msg( "CCollectionInfoHTTPRequest: Could not parse response for collection info\n" ); } }