csgo-2018-source/game/server/dedicated_server_ugc_manager.cpp
2021-07-24 21:11:47 -07:00

1477 lines
47 KiB
C++

//========== 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 <fileid>\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 <fileid>\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 <fileid>\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<PublishedFileId_t>& 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<CUtlString> 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<PublishedFileId_t>& 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 : "<no title>" );
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 : "<no title>" );
}
}
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<const DedicatedServerUGCFileInfo_t*>& 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<const DedicatedServerUGCFileInfo_t *>& 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<PublishedFileId_t>& 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<PublishedFileId_t> 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 : "<no title>" );
}
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<CUtlString> 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 : "<no title>", 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<PublishedFileId_t> &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<PublishedFileId_t>& 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<PublishedFileId_t>& 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" );
}
}