Counter Strike : Global Offensive Source Code
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.

1476 lines
47 KiB

  1. //========== Copyright Valve Corporation, All rights reserved. ========
  2. //
  3. // Purpose:
  4. //
  5. //=============================================================================
  6. #include "cbase.h"
  7. #include "dedicated_server_ugc_manager.h"
  8. #include "steam/isteamhttp.h"
  9. #include "ugc_utils.h"
  10. #include "tier2/fileutils.h"
  11. #include "gametypes.h"
  12. // TODO: can we swap this out based on steam universe?
  13. const char* g_szAuthKeyFilename = "webapi_authkey.txt";
  14. const char* g_szCollectionCacheFileName = "ugc_collection_cache.txt";
  15. const char* g_szSubscribedFilesList = "subscribed_file_ids.txt";
  16. const char* g_szSubscribedCollectionsList = "subscribed_collection_ids.txt";
  17. // Subdir relative to game dir to store workshop maps in
  18. const char* g_szWorkshopMapBasePath = "maps/workshop";
  19. const char* GetApiBaseUrl( void )
  20. {
  21. return "https://api.steampowered.com";
  22. /*
  23. if ( steamapicontext && steamapicontext->SteamUtils() )
  24. {
  25. if ( steamapicontext->SteamUtils()->GetConnectedUniverse() == k_EUniverseBeta )
  26. return "https://api-beta.steampowered.com";
  27. else if ( steamapicontext->SteamUtils()->GetConnectedUniverse() == k_EUniversePublic )
  28. return "https://api.steampowered.com";
  29. }
  30. Assert( 0 );
  31. return "";
  32. */
  33. }
  34. ConVar sv_debug_ugc_downloads( "sv_debug_ugc_downloads", "0", FCVAR_RELEASE );
  35. ConVar sv_broadcast_ugc_downloads( "sv_broadcast_ugc_downloads", "0", FCVAR_RELEASE );
  36. ConVar sv_broadcast_ugc_download_progress_interval( "sv_broadcast_ugc_download_progress_interval", "8", FCVAR_RELEASE );
  37. ConVar sv_ugc_manager_max_new_file_check_interval_secs( "sv_ugc_manager_max_new_file_check_interval_secs", "1000", FCVAR_RELEASE );
  38. ConVar sv_remove_old_ugc_downloads( "sv_remove_old_ugc_downloads", "1", FCVAR_RELEASE );
  39. ConVar sv_test_steam_connection_failure( "sv_test_steam_connection_failure", "0" );
  40. CDedicatedServerWorkshopManager g_DedicatedServerWorkshopManager;
  41. CDedicatedServerWorkshopManager& DedicatedServerWorkshop( void )
  42. {
  43. return g_DedicatedServerWorkshopManager;
  44. }
  45. 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." )
  46. {
  47. if ( !UTIL_IsCommandIssuedByServerAdmin() )
  48. return;
  49. if ( args.ArgC() != 2 )
  50. {
  51. Msg( "Usage: workshop_start_map <fileid>\n");
  52. return;
  53. }
  54. PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] );
  55. if ( id == 0 )
  56. {
  57. Msg( "Invalid file id.\n");
  58. return;
  59. }
  60. DedicatedServerWorkshop().SetTargetStartMap( id );
  61. }
  62. CON_COMMAND( host_workshop_map, "Get the latest version of the map and host it on this server." )
  63. {
  64. if ( !UTIL_IsCommandIssuedByServerAdmin() )
  65. return;
  66. if ( args.ArgC() != 2 )
  67. {
  68. Msg( "Usage: host_workshop_map <fileid>\n");
  69. return;
  70. }
  71. PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] );
  72. if ( id == 0 )
  73. {
  74. Msg( "Invalid file id.\n");
  75. return;
  76. }
  77. // HACK: Need to load a map for steam server api to be available and download maps... peeling out the init code
  78. // would be better, but loading dust works for now.
  79. if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() )
  80. engine->ServerCommand( CFmtStr( "map de_dust server_is_unavailable\n" ).Access() );
  81. DedicatedServerWorkshop().HostWorkshopMap( id );
  82. }
  83. CON_COMMAND( host_workshop_collection, "Get the latest version of maps in a workshop collection and host them as a maplist." )
  84. {
  85. if ( !UTIL_IsCommandIssuedByServerAdmin() )
  86. return;
  87. if ( args.ArgC() != 2 )
  88. {
  89. Msg( "Usage: host_workshop_collection <fileid>\n");
  90. return;
  91. }
  92. PublishedFileId_t id = (PublishedFileId_t)V_atoui64( args[1] );
  93. if ( id == 0 )
  94. {
  95. Msg( "Invalid file id.\n");
  96. return;
  97. }
  98. // HACK: Need to load a map for steam server api to be available and download maps... peeling out the init code
  99. // would be better, but loading dust works for now.
  100. if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() )
  101. engine->ServerCommand( CFmtStr( "map de_dust server_is_unavailable\n" ).Access() );
  102. DedicatedServerWorkshop().HostWorkshopMapCollection( id );
  103. }
  104. bool DedicatedServerUGCFileInfo_t::BuildFromKV( KeyValues *pPublishedFileDetails )
  105. {
  106. m_bIsValid = false;
  107. m_result = (EResult)pPublishedFileDetails->GetInt( "result", -1 );
  108. // Parse file id first, we should get this even on failures.
  109. fileId = pPublishedFileDetails->GetUint64( "publishedfileid", 0ll );
  110. if ( !fileId )
  111. return false;
  112. if ( m_result != k_EResultOK )
  113. return false;
  114. int appId = pPublishedFileDetails->GetInt( "consumer_appid", 0 );
  115. if ( appId != engine->GetAppID() )
  116. return false;
  117. if ( pPublishedFileDetails->GetInt( "banned", 0 ) != 0 )
  118. return false;
  119. contentHandle = pPublishedFileDetails->GetUint64( "hcontent_file", 0ll );
  120. if ( !contentHandle )
  121. return false;
  122. const char* szUrl = pPublishedFileDetails->GetString( "file_url", NULL );
  123. if ( !szUrl )
  124. return false;
  125. V_strcpy_safe( m_szUrl, szUrl );
  126. const char* szName = V_UnqualifiedFileName( pPublishedFileDetails->GetString( "filename", NULL ) );
  127. if ( !szName )
  128. return false;
  129. V_strcpy_safe( m_szFileName, szName );
  130. m_unFileSizeInBytes = pPublishedFileDetails->GetInt( "file_size", 0 );
  131. if ( !m_unFileSizeInBytes )
  132. return false;
  133. const char* szTitle = pPublishedFileDetails->GetString( "title", NULL );
  134. if ( !szTitle )
  135. return false;
  136. V_strcpy_safe( m_szTitle, szTitle );
  137. m_unTimeLastUpdated = pPublishedFileDetails->GetInt( "time_updated", 0 );
  138. if ( !m_unTimeLastUpdated )
  139. return false;
  140. // Assuming we're downloading maps here...
  141. V_snprintf( m_szFilePath, ARRAYSIZE(m_szFilePath), "%s/%llu/%s", g_szWorkshopMapBasePath, fileId, szName );
  142. // TODO tags
  143. m_bIsValid = true;
  144. m_dblPlatFloatTimeReceived = Plat_FloatTime();
  145. return true;
  146. }
  147. void ParseFileIds( const char* szFileName, CUtlVector<PublishedFileId_t>& outVec )
  148. {
  149. if ( filesystem->FileExists( szFileName ) )
  150. {
  151. int fileSize;
  152. char* szFileBuf = (char*)UTIL_LoadFileForMe( szFileName, &fileSize );
  153. if ( szFileBuf && fileSize > 0 )
  154. {
  155. CUtlStringList fileIdList;
  156. V_SplitString( szFileBuf, "\n", fileIdList );
  157. for ( int i = 0; i < fileIdList.Count(); ++i )
  158. {
  159. PublishedFileId_t id = V_atoui64( fileIdList[i] );
  160. if ( !outVec.HasElement( id ) )
  161. outVec.AddToTail( id );
  162. if ( sv_debug_ugc_downloads.GetBool() )
  163. Msg( "CDedicatedServerWorkshopManager::Init: Subscribing to file id %llu\n", id );
  164. }
  165. delete[] szFileBuf;
  166. }
  167. }
  168. }
  169. bool CDedicatedServerWorkshopManager::Init( void )
  170. {
  171. m_UGCFileInfos.SetLessFunc( DefLessFunc( PublishedFileId_t ) );
  172. m_mapWorkshopIdsToMapNames.SetLessFunc( DefLessFunc( PublishedFileId_t) );
  173. m_mapPreviousCollectionQueryCache.SetLessFunc( DefLessFunc( PublishedFileId_t) );
  174. m_fTimeLastVersionCheck = 0;
  175. GetNewestSubscribedFiles();
  176. // HACK: So if we load a map before we hear back from steam about what files we have subscribed,
  177. // we won't submit any workshop map IDs to matchmaking. Scan for any maps in the workshop subdirectory
  178. // and use any bsps found as available maps.
  179. CUtlVector<CUtlString> outList;
  180. RecursiveFindFilesMatchingName( &outList, g_szWorkshopMapBasePath, "*.bsp", "MOD" );
  181. FOR_EACH_VEC( outList, i )
  182. {
  183. CUtlString &curMap = outList[i];
  184. PublishedFileId_t id = GetUGCMapPublishedFileID( curMap.Access() );
  185. if ( id != 0 )
  186. {
  187. NoteWorkshopMapOnDisk( id, curMap.Access() );
  188. }
  189. }
  190. m_bHostedCollectionUpdatePending = false ;
  191. m_unTargetStartMap = 0;
  192. if ( g_pFullFileSystem->FileExists( g_szCollectionCacheFileName, "MOD" ) )
  193. {
  194. KeyValues* pCollectionCacheKV = new KeyValues("");
  195. KeyValues::AutoDelete autodelete( pCollectionCacheKV );
  196. pCollectionCacheKV->LoadFromFile( g_pFullFileSystem, g_szCollectionCacheFileName, "MOD" );
  197. for ( KeyValues *pDetails = pCollectionCacheKV->GetFirstSubKey(); pDetails != NULL; pDetails = pDetails->GetNextKey() )
  198. {
  199. PublishedFileId_t collectionId = pDetails->GetUint64( "publishedfileid", 0 );
  200. if ( collectionId != 0 )
  201. {
  202. m_mapPreviousCollectionQueryCache.Insert( collectionId, pDetails->MakeCopy() );
  203. }
  204. }
  205. }
  206. return true;
  207. }
  208. void CDedicatedServerWorkshopManager::LevelInitPreEntity( void )
  209. {
  210. // reset these every level change.
  211. m_hackCurrentMapInfoCheck = 0;
  212. m_bCurrentLevelNeedsUpdate = false;
  213. m_bHostedCollectionUpdatePending = false;
  214. }
  215. void CDedicatedServerWorkshopManager::CheckIfCurrentLevelNeedsUpdate( void )
  216. {
  217. m_bCurrentLevelNeedsUpdate = false;
  218. PublishedFileId_t id = GetUGCMapPublishedFileID( gpGlobals->mapname.ToCStr() );
  219. if ( id != 0 )
  220. {
  221. m_hackCurrentMapInfoCheck = id;
  222. if ( !m_FileInfoQueries.HasElement( id ) )
  223. m_FileInfoQueries.AddToTail( id );
  224. }
  225. }
  226. 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 )
  227. {
  228. g_DedicatedServerWorkshopManager.GetNewestSubscribedFiles();
  229. }
  230. void CDedicatedServerWorkshopManager::GetNewestSubscribedFiles( void )
  231. {
  232. if ( sv_debug_ugc_downloads.GetBool() )
  233. Msg("CDedicatedServerWorkshopManager::GetNewestSubscribedFiles\n");
  234. if ( engine->IsDedicatedServer() )
  235. {
  236. Q_memset( m_szWebAPIAuthKey, 0, ARRAYSIZE( m_szWebAPIAuthKey ) );
  237. const char *szAuthKey = CommandLine()->ParmValue( "-authkey", "" );
  238. if ( !StringIsEmpty( szAuthKey ) )
  239. {
  240. V_strcpy_safe( m_szWebAPIAuthKey, szAuthKey );
  241. }
  242. else if ( filesystem->FileExists( g_szAuthKeyFilename, "MOD" ) )
  243. {
  244. int nLength;
  245. szAuthKey = (const char *)UTIL_LoadFileForMe( g_szAuthKeyFilename, &nLength );
  246. if ( szAuthKey != NULL )
  247. {
  248. if ( !StringIsEmpty( szAuthKey ) )
  249. {
  250. V_strcpy_safe( m_szWebAPIAuthKey, szAuthKey );
  251. int len = strlen(m_szWebAPIAuthKey);
  252. while ( len > 0 && V_isspace(m_szWebAPIAuthKey[len-1]) )
  253. {
  254. m_szWebAPIAuthKey[len-1] = 0;
  255. len--;
  256. }
  257. if (len>0)
  258. {
  259. Msg( "Loaded authkey from %s: %s\n", g_szAuthKeyFilename, m_szWebAPIAuthKey );
  260. }
  261. }
  262. UTIL_FreeFile( (byte *)szAuthKey );
  263. szAuthKey = NULL;
  264. }
  265. if ( StringIsEmpty(m_szWebAPIAuthKey) )
  266. {
  267. Msg( "Auth key file %s not valid\n", g_szAuthKeyFilename );
  268. }
  269. }
  270. if ( ! StringIsEmpty(m_szWebAPIAuthKey) )
  271. {
  272. m_bFoundAuthKey = true;
  273. if ( sv_debug_ugc_downloads.GetBool() )
  274. Msg( "CDedicatedServerWorkshopManager::Init: Using auth key [%s]\n", m_szWebAPIAuthKey );
  275. }
  276. else
  277. {
  278. m_bFoundAuthKey = false;
  279. Msg( "No web api auth key specified - workshop downloads will be disabled.\n" );
  280. }
  281. if ( m_bFoundAuthKey )
  282. {
  283. //TODO: protect double adds?
  284. ParseFileIds( g_szSubscribedFilesList, m_FileInfoQueries );
  285. m_vecMapsBeingUpdated.AddVectorToTail( m_FileInfoQueries );
  286. ParseFileIds( g_szSubscribedCollectionsList, m_CollectionInfoQueries );
  287. // If we're hosting a workshop map collection, get the latest version of those maps
  288. PublishedFileId_t id = V_atoui64( gpGlobals->mapGroupName.ToCStr() );
  289. if ( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) && id != 0 )
  290. {
  291. // 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
  292. const CUtlStringList * pMapList = g_pGameTypes->GetMapGroupMapList( gpGlobals->mapGroupName.ToCStr() );
  293. if ( pMapList->Count() == 1 && GetUGCMapPublishedFileID( (*pMapList)[0] ) == id )
  294. {
  295. UpdateFile( id );
  296. }
  297. else
  298. {
  299. m_CollectionInfoQueries.AddToTail( id );
  300. }
  301. }
  302. m_fTimeLastVersionCheck = Plat_FloatTime();
  303. }
  304. }
  305. }
  306. void CDedicatedServerWorkshopManager::Shutdown( void )
  307. {
  308. KeyValues* pOutKV = new KeyValues( "CollectionInfoCache" );
  309. KeyValues::AutoDelete autodelete( pOutKV );
  310. FOR_EACH_MAP( m_mapPreviousCollectionQueryCache, i )
  311. {
  312. pOutKV->AddSubKey( m_mapPreviousCollectionQueryCache[i]->MakeCopy() );
  313. m_mapPreviousCollectionQueryCache[i]->deleteThis();
  314. }
  315. pOutKV->SaveToFile( g_pFullFileSystem, g_szCollectionCacheFileName, "MOD" );
  316. Cleanup();
  317. }
  318. void CDedicatedServerWorkshopManager::Cleanup( void )
  319. {
  320. FOR_EACH_VEC_BACK( m_PendingFileDownloads, i )
  321. {
  322. delete m_PendingFileDownloads[i];
  323. m_PendingFileDownloads.Remove( i );
  324. }
  325. m_UGCFileInfos.PurgeAndDeleteElements();
  326. m_FileInfoQueries.RemoveAll();
  327. m_CollectionInfoQueries.RemoveAll();
  328. m_vecWorkshopMapList.RemoveAll();
  329. m_mapWorkshopIdsToMapNames.RemoveAll();
  330. m_vecMapsBeingUpdated.RemoveAll();
  331. m_bFoundAuthKey = false;
  332. m_desiredHostCollection = 0;
  333. m_bHostedCollectionUpdatePending = false;
  334. if ( m_pMapGroupBuilder )
  335. {
  336. delete m_pMapGroupBuilder;
  337. m_pMapGroupBuilder = NULL;
  338. }
  339. }
  340. void CDedicatedServerWorkshopManager::Update( void )
  341. {
  342. if ( !m_bFoundAuthKey )
  343. return;
  344. if ( steamgameserverapicontext == NULL )
  345. return;
  346. UpdatePublishedFileInfoRequests();
  347. UpdateUGCDownloadRequests();
  348. if ( m_pMapGroupBuilder )
  349. {
  350. if ( m_pMapGroupBuilder->IsFinished() )
  351. {
  352. m_pMapGroupBuilder->CreateOrUpdateMapGroup();
  353. if ( m_desiredHostCollection != 0 && m_pMapGroupBuilder->GetFirstMap() != NULL )
  354. {
  355. // Set the map group and changelevel if this was our target hosting map group
  356. const char* szStartMap = m_unTargetStartMap ? m_pMapGroupBuilder->GetMapMatchingId( m_unTargetStartMap ) : m_pMapGroupBuilder->GetFirstMap();
  357. engine->ServerCommand( CFmtStr( "mapgroup %llu;map %s\n", m_pMapGroupBuilder->GetId(), szStartMap ).Access() );
  358. m_unTargetStartMap = 0;
  359. }
  360. delete m_pMapGroupBuilder;
  361. m_pMapGroupBuilder = NULL;
  362. m_desiredHostCollection = 0;
  363. }
  364. }
  365. }
  366. bool CDedicatedServerWorkshopManager::ShouldUpdateCollection( PublishedFileId_t id, const CUtlVector<PublishedFileId_t>& vecMaps )
  367. {
  368. // Special case for hosted collection updates: Don't re-query all the file infos if our collection contents hasn't changed.
  369. // For large collections in locations with high ping to steam, getting all that file info takes too long and hangs up level changes.
  370. const char *szMapGroup = gpGlobals->mapGroupName.ToCStr();
  371. bool bUpdateCollectionFiles = true;
  372. if ( m_bHostedCollectionUpdatePending && g_pGameTypes->IsWorkshopMapGroup( szMapGroup ) )
  373. {
  374. PublishedFileId_t curHostedCollectionID = V_atoui64( szMapGroup );
  375. Assert ( curHostedCollectionID != 0 ); // map groups hosted by dedicated servers should always have a uint64 name
  376. if ( curHostedCollectionID == id )
  377. {
  378. const CUtlStringList &maplist = *g_pGameTypes->GetMapGroupMapList( szMapGroup );
  379. // NOTE: this gives false positives if the collection contains invalid ids (eg, removed from workshop, etc)
  380. // because bad items in the list won't end up in the final map group, causing the count mismatch...
  381. bool bChanged = maplist.Count() != vecMaps.Count();
  382. if ( !bChanged )
  383. {
  384. // If count matches, make sure the contents are the same. If so, we can skip updating the collection
  385. FOR_EACH_VEC( maplist, i )
  386. {
  387. PublishedFileId_t id = GetUGCMapPublishedFileID( maplist[i] );
  388. if ( vecMaps.Find( id ) == -1 )
  389. {
  390. bChanged = true;
  391. break;
  392. }
  393. }
  394. }
  395. bUpdateCollectionFiles = bChanged;
  396. }
  397. // Make sure we mark our collection as no longer updating
  398. if ( ( id == m_desiredHostCollection ) || ( id == curHostedCollectionID ) )
  399. {
  400. m_bHostedCollectionUpdatePending = false;
  401. }
  402. }
  403. return bUpdateCollectionFiles;
  404. }
  405. void CDedicatedServerWorkshopManager::UpdatePublishedFileInfoRequests( void )
  406. {
  407. // Process finished queries
  408. FOR_EACH_VEC_BACK( m_PendingFileInfoRequests, i )
  409. {
  410. CPublishedFileInfoHTTPRequest* pCurRequest = m_PendingFileInfoRequests[i];
  411. if( !pCurRequest->IsFinished() ) // still waiting on a response
  412. continue;
  413. if ( pCurRequest->GetLastHTTPResult() != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() )
  414. {
  415. // Handle http errors, retries
  416. // Remove failed map ids from the pending list
  417. if ( sv_debug_ugc_downloads.GetBool() )
  418. Msg( "Failed to get file info information from steam, HTTP status: %d\n. Missing info for file ids: ", pCurRequest->GetLastHTTPResult() );
  419. FOR_EACH_VEC( pCurRequest->GetItemsQueried(), i )
  420. {
  421. PublishedFileId_t id = pCurRequest->GetItemsQueried()[i];
  422. OnFileInfoRequestFailed( id );
  423. if ( sv_debug_ugc_downloads.GetBool() )
  424. Msg( "%llu ", id );
  425. }
  426. if ( sv_debug_ugc_downloads.GetBool() )
  427. Msg( "\n" );
  428. }
  429. else
  430. {
  431. FOR_EACH_VEC( pCurRequest->GetFileInfoList(), j )
  432. {
  433. const DedicatedServerUGCFileInfo_t* pCurInfo = pCurRequest->GetFileInfoList()[j];
  434. OnFileInfoReceived( pCurInfo );
  435. if ( pCurInfo->m_result == k_EResultOK && pCurInfo->m_bIsValid )
  436. {
  437. if ( sv_debug_ugc_downloads.GetBool() )
  438. Msg( "CDedicatedServerWorkshopManager: received file details for id %llu: '%s'.\n", pCurInfo->fileId, pCurInfo->m_szTitle ? pCurInfo->m_szTitle : "<no title>" );
  439. RemoveFileInfo( pCurInfo->fileId ); // Clear any existing entry and cancel any pending download.
  440. DedicatedServerUGCFileInfo_t* pNewFileInfo = new DedicatedServerUGCFileInfo_t;
  441. V_memcpy( (void*)pNewFileInfo, (void*)pCurInfo, sizeof ( DedicatedServerUGCFileInfo_t ) );
  442. m_UGCFileInfos.Insert( pNewFileInfo->fileId, pNewFileInfo );
  443. // Skip downloading if this is an 'info only' id.
  444. if ( m_hackCurrentMapInfoCheck == pNewFileInfo->fileId )
  445. {
  446. m_hackCurrentMapInfoCheck = 0;
  447. continue;
  448. }
  449. if ( !IsFileLatestVersion( pNewFileInfo ) )
  450. {
  451. QueueDownloadFile( pNewFileInfo );
  452. }
  453. else
  454. {
  455. OnFileDownloaded( pNewFileInfo );
  456. if ( sv_debug_ugc_downloads.GetBool() )
  457. Msg( "Skipping download for file id %llu:'%s' - version on disk is latest.\n", pNewFileInfo->fileId, pNewFileInfo->m_szTitle ? pNewFileInfo->m_szTitle : "<no title>" );
  458. }
  459. }
  460. else
  461. {
  462. if ( sv_debug_ugc_downloads.GetBool() )
  463. Msg( "Failed to parse file details KV for id %llu. Result enum: %d\n", pCurInfo->fileId, pCurInfo->m_result );
  464. if ( pCurInfo->m_result == k_EResultFileNotFound )
  465. Msg( "File id %llu not found. Probably removed from workshop\n", pCurInfo->fileId );
  466. }
  467. }
  468. }
  469. delete pCurRequest; // request dealt with
  470. m_PendingFileInfoRequests.Remove( i );
  471. }
  472. FOR_EACH_VEC_BACK( m_PendingCollectionInfoRequests, i )
  473. {
  474. CCollectionInfoHTTPRequest *pCurRequest = m_PendingCollectionInfoRequests[i];
  475. if ( !pCurRequest->IsFinished() )
  476. continue;
  477. if ( pCurRequest->GetLastHTTPResult() != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() )
  478. {
  479. if ( sv_debug_ugc_downloads.GetBool() )
  480. Msg( "Failed to get file info information from steam, HTTP status: %d\n. Missing info for collection ids: ", pCurRequest->GetLastHTTPResult() );
  481. FOR_EACH_VEC( pCurRequest->GetItemsQueried(), i )
  482. {
  483. PublishedFileId_t id = pCurRequest->GetItemsQueried()[i];
  484. OnCollectionInfoRequestFailed( id );
  485. if ( sv_debug_ugc_downloads.GetBool() )
  486. Msg( "%llu ", id );
  487. if ( id == m_desiredHostCollection )
  488. {
  489. int idx = m_mapPreviousCollectionQueryCache.Find( id );
  490. if ( idx != m_mapPreviousCollectionQueryCache.InvalidIndex() )
  491. {
  492. ParseCollectionInfo( m_mapPreviousCollectionQueryCache[idx] );
  493. }
  494. else
  495. {
  496. m_bHostedCollectionUpdatePending = false; // failed to get info on host collection, and we didn't have it in our cache
  497. }
  498. }
  499. }
  500. if ( sv_debug_ugc_downloads.GetBool() )
  501. Msg( "\n" );
  502. }
  503. else
  504. {
  505. KeyValues* pCollectionDetails = pCurRequest->GetResponseKV();
  506. for ( KeyValues *pDetails = pCollectionDetails->GetFirstSubKey(); pDetails != NULL; pDetails = pDetails->GetNextKey() )
  507. {
  508. PublishedFileId_t collectionId = ParseCollectionInfo( pDetails );
  509. if ( collectionId != 0 )
  510. {
  511. // Save previously queried collection infos to disk in case we lose connection to steam
  512. int idx = m_mapPreviousCollectionQueryCache.Find( collectionId );
  513. if ( idx == m_mapPreviousCollectionQueryCache.InvalidIndex() )
  514. idx = m_mapPreviousCollectionQueryCache.Insert( collectionId );
  515. else
  516. m_mapPreviousCollectionQueryCache[idx]->deleteThis();
  517. m_mapPreviousCollectionQueryCache[idx] = pDetails->MakeCopy();
  518. }
  519. }
  520. }
  521. delete pCurRequest;
  522. m_PendingCollectionInfoRequests.Remove( i );
  523. }
  524. if ( m_FileInfoQueries.Count() > 0 )
  525. {
  526. CPublishedFileInfoHTTPRequest *pRequest = new CPublishedFileInfoHTTPRequest( m_FileInfoQueries );
  527. pRequest->CreateHTTPRequest( m_szWebAPIAuthKey );
  528. m_PendingFileInfoRequests.AddToTail( pRequest );
  529. m_FileInfoQueries.RemoveAll();
  530. }
  531. if ( m_CollectionInfoQueries.Count() > 0 )
  532. {
  533. CCollectionInfoHTTPRequest *pRequest = new CCollectionInfoHTTPRequest( m_CollectionInfoQueries );
  534. pRequest->CreateHTTPRequest( m_szWebAPIAuthKey );
  535. m_PendingCollectionInfoRequests.AddToTail( pRequest );
  536. m_CollectionInfoQueries.RemoveAll();
  537. }
  538. }
  539. void CDedicatedServerWorkshopManager::UpdateUGCDownloadRequests( void )
  540. {
  541. if ( m_PendingFileDownloads.Count() )
  542. {
  543. // TODO: Handle timeouts/errors?
  544. m_PendingFileDownloads[0]->Update();
  545. if ( m_PendingFileDownloads[0]->IsFinished() )
  546. {
  547. OnFileDownloaded( m_PendingFileDownloads[0]->GetFileInfo() );
  548. delete m_PendingFileDownloads[0];
  549. m_PendingFileDownloads.Remove( 0 );
  550. }
  551. }
  552. /*
  553. BUG/TODO: Downloading lots of files at the same time runs out of memory
  554. in GetHTTPResponseBodyData growing a buffer... Only downloading one at a time for now.
  555. FOR_EACH_VEC_BACK( m_PendingFileDownloads, i )
  556. {
  557. m_PendingFileDownloads[i]->Update();
  558. if ( m_PendingFileDownloads[i]->IsFinished() )
  559. {
  560. delete m_PendingFileDownloads[i];
  561. m_PendingFileDownloads.Remove( i );
  562. }
  563. }
  564. */
  565. }
  566. void CDedicatedServerWorkshopManager::QueueDownloadFile( const DedicatedServerUGCFileInfo_t *pFileInfo )
  567. {
  568. CStreamingUGCDownloader *pDownloader = new CStreamingUGCDownloader;
  569. pDownloader->StartFileDownload( pFileInfo, 1024*1024 );
  570. m_PendingFileDownloads.AddToTail( pDownloader );
  571. }
  572. bool CDedicatedServerWorkshopManager::IsFileLatestVersion( const DedicatedServerUGCFileInfo_t* ugcInfo )
  573. {
  574. // never try to update an official map, they're shipped with the depot
  575. if ( UGCUtil_IsOfficialMap( ugcInfo->fileId ) )
  576. return true;
  577. if ( g_pFullFileSystem->FileExists( ugcInfo->m_szFilePath ) )
  578. {
  579. // mtime needs to match the time last updated exactly, as we slam the file time when we download
  580. // so an earlier time is out of date and a later time may be modified due to file copying.
  581. uint32 fileTime = (uint32)g_pFullFileSystem->GetFileTime( ugcInfo->m_szFilePath );
  582. if ( ugcInfo->m_unTimeLastUpdated == fileTime )
  583. return true;
  584. }
  585. return false;
  586. }
  587. void CDedicatedServerWorkshopManager::RemoveFileInfo( PublishedFileId_t id )
  588. {
  589. unsigned short idx = m_UGCFileInfos.Find( id );
  590. if ( idx != m_UGCFileInfos.InvalidIndex() )
  591. {
  592. // Cancel any pending download
  593. FOR_EACH_VEC_BACK( m_PendingFileDownloads, i )
  594. {
  595. if ( m_PendingFileDownloads[i]->GetPublishedFileId() == id )
  596. {
  597. delete m_PendingFileDownloads[i];
  598. m_PendingFileDownloads.Remove( i );
  599. }
  600. }
  601. delete m_UGCFileInfos[idx];
  602. m_UGCFileInfos.RemoveAt( idx );
  603. }
  604. }
  605. bool CDedicatedServerWorkshopManager::GetMapsMatchingName( const char* szMapName, CUtlVector<const DedicatedServerUGCFileInfo_t*>& outVec ) const
  606. {
  607. bool bFoundAny = false;
  608. FOR_EACH_MAP( m_UGCFileInfos, i )
  609. {
  610. const DedicatedServerUGCFileInfo_t *pInfo = m_UGCFileInfos[i];
  611. // compare just map name (ignore extension and paths passed in)
  612. char szBaseQueryMapName[MAX_PATH], szBaseUGCMapName[MAX_PATH];
  613. V_FileBase( szMapName, szBaseQueryMapName, ARRAYSIZE( szBaseQueryMapName ) );
  614. V_FileBase( pInfo->m_szFileName, szBaseUGCMapName, ARRAYSIZE( szBaseUGCMapName ) );
  615. if ( V_strcmp( szBaseQueryMapName, szBaseUGCMapName ) == 0 )
  616. {
  617. outVec.AddToTail( pInfo );
  618. bFoundAny = true;
  619. }
  620. }
  621. return bFoundAny;
  622. }
  623. // Get the maps for which we downloaded UGC information successfully
  624. void CDedicatedServerWorkshopManager::GetWorkshopMasWithValidUgcInformation( CUtlVector<const DedicatedServerUGCFileInfo_t *>& outVec ) const
  625. {
  626. FOR_EACH_MAP( m_UGCFileInfos, i )
  627. {
  628. const DedicatedServerUGCFileInfo_t *pInfo = m_UGCFileInfos[i];
  629. if ( !pInfo )
  630. continue;
  631. outVec.AddToTail( pInfo );
  632. }
  633. }
  634. // HACK: Using the map's directory to get the published file id...
  635. PublishedFileId_t CDedicatedServerWorkshopManager::GetUGCMapPublishedFileID( const char* szPathToUGCMap ) const
  636. {
  637. char tmp[MAX_PATH];
  638. V_strcpy_safe( tmp, szPathToUGCMap );
  639. V_FixSlashes( tmp, '/' ); // internal path strings use forward slashes, make sure we compare like that.
  640. //if ( !V_strncmp( tmp, g_szWorkshopMapBasePath, strlen( g_szWorkshopMapBasePath ) ) )
  641. if ( V_strstr( tmp, "workshop/" ) )
  642. {
  643. V_StripFilename(tmp);
  644. V_StripTrailingSlash(tmp);
  645. const char* szDirName = V_GetFileName(tmp);
  646. return (PublishedFileId_t)V_atoui64(szDirName);
  647. }
  648. /*
  649. char szBspNoExtension[MAX_PATH];
  650. V_strcpy_safe( szBspNoExtension, szPathToUGCMap );
  651. V_StripExtension( szBspNoExtension, szBspNoExtension, sizeof( szBspNoExtension ) );
  652. char szElemPathNoExt[MAX_PATH];
  653. FOR_EACH_MAP( m_UGCFileInfos, i )
  654. {
  655. DedicatedServerUGCFileInfo_t* pElem = m_UGCFileInfos.Element(i);
  656. if ( !V_stricmp( szPathToUGCMap, szBSPPath ) )
  657. {
  658. return m_UGCFileInfos.Key(i);
  659. }
  660. }
  661. */
  662. return 0;
  663. }
  664. const CUtlVector< PublishedFileId_t > & CDedicatedServerWorkshopManager::GetWorkshopMapList( void ) const
  665. {
  666. return m_vecWorkshopMapList;
  667. }
  668. // Records all workshop maps we have on disk (may or may not be up to date).
  669. void CDedicatedServerWorkshopManager::NoteWorkshopMapOnDisk( PublishedFileId_t id, const char* szPath )
  670. {
  671. if ( m_vecWorkshopMapList.Find( id ) == m_vecWorkshopMapList.InvalidIndex() )
  672. {
  673. m_vecWorkshopMapList.AddToTail( id );
  674. int idx = m_mapWorkshopIdsToMapNames.Insert( id );
  675. m_mapWorkshopIdsToMapNames[idx].Set( szPath );
  676. V_FixSlashes( m_mapWorkshopIdsToMapNames[idx].Get(), '/' ); // Always refer to map paths with forward slashes internally for consistancy.
  677. }
  678. }
  679. void CDedicatedServerWorkshopManager::HostWorkshopMap( PublishedFileId_t id )
  680. {
  681. UpdateFile( id );
  682. m_desiredHostCollection = id;
  683. }
  684. void CDedicatedServerWorkshopManager::HostWorkshopMapCollection( PublishedFileId_t id )
  685. {
  686. if ( m_bFoundAuthKey == false )
  687. {
  688. Warning( "host_workshop_collection: Web API auth key not found!\n" );
  689. }
  690. if ( !m_CollectionInfoQueries.HasElement( id ) )
  691. m_CollectionInfoQueries.AddToTail( id );
  692. m_desiredHostCollection = id;
  693. }
  694. // Called each time we get ugc file metadata from steam. Assumes this will get called before OnFileDownloaded.
  695. void CDedicatedServerWorkshopManager::OnFileInfoReceived( const DedicatedServerUGCFileInfo_t *pInfo )
  696. {
  697. if ( pInfo->m_result != k_EResultOK )
  698. {
  699. if ( m_pMapGroupBuilder )
  700. {
  701. m_pMapGroupBuilder->RemoveRequiredMap( pInfo->fileId );
  702. }
  703. m_vecMapsBeingUpdated.FindAndFastRemove( pInfo->fileId );
  704. }
  705. // Host single maps as a collection of one
  706. if ( pInfo->fileId == m_desiredHostCollection )
  707. {
  708. if ( m_pMapGroupBuilder )
  709. delete m_pMapGroupBuilder;
  710. CUtlVector< PublishedFileId_t > vecMapFile;
  711. vecMapFile.AddToTail( pInfo->fileId );
  712. m_pMapGroupBuilder = new CWorkshopMapGroupBuilder( pInfo->fileId, vecMapFile );
  713. }
  714. if( pInfo->fileId == m_hackCurrentMapInfoCheck )
  715. {
  716. m_bCurrentLevelNeedsUpdate = !IsFileLatestVersion( pInfo );
  717. }
  718. }
  719. void CDedicatedServerWorkshopManager::OnFileInfoRequestFailed( PublishedFileId_t id )
  720. {
  721. m_vecMapsBeingUpdated.FindAndRemove( id ); // no longer being updated
  722. if ( m_pMapGroupBuilder )
  723. {
  724. m_pMapGroupBuilder->RemoveRequiredMap( id );
  725. }
  726. }
  727. void CDedicatedServerWorkshopManager::OnCollectionInfoReceived( PublishedFileId_t collectionId, const CUtlVector< PublishedFileId_t > & vecCollectionItems )
  728. {
  729. if ( vecCollectionItems.Count() > 0 )
  730. {
  731. PublishedFileId_t curHostedCollection = 0;
  732. if( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) )
  733. {
  734. curHostedCollection = V_atoui64( gpGlobals->mapGroupName.ToCStr() );
  735. }
  736. // Make/refresh a mapgroup if it's the one we want to/are hosting.
  737. if ( collectionId == m_desiredHostCollection || collectionId == curHostedCollection )
  738. {
  739. if ( m_pMapGroupBuilder )
  740. delete m_pMapGroupBuilder;
  741. m_pMapGroupBuilder = new CWorkshopMapGroupBuilder( collectionId, vecCollectionItems );
  742. m_bHostedCollectionUpdatePending = false;
  743. }
  744. }
  745. }
  746. void CDedicatedServerWorkshopManager::OnCollectionInfoRequestFailed( PublishedFileId_t id )
  747. {
  748. }
  749. void CDedicatedServerWorkshopManager::OnFileDownloaded( const DedicatedServerUGCFileInfo_t *pInfo )
  750. {
  751. if ( m_pMapGroupBuilder )
  752. {
  753. m_pMapGroupBuilder->OnMapDownloaded( pInfo );
  754. }
  755. m_vecMapsBeingUpdated.FindAndFastRemove( pInfo->fileId );
  756. NoteWorkshopMapOnDisk( pInfo->fileId, pInfo->m_szFilePath );
  757. }
  758. bool CDedicatedServerWorkshopManager::HasPendingMapDownloads( void ) const
  759. {
  760. if ( !steamgameserverapicontext || !steamgameserverapicontext->SteamHTTP() || !steamgameserverapicontext->SteamGameServer() || !steamgameserverapicontext->SteamGameServer()->BLoggedOn() || (engine->GetGameServerSteamID() && engine->GetGameServerSteamID()->GetEAccountType() == k_EAccountTypeInvalid) )
  761. return false;
  762. // TODO: Timeouts, errors
  763. // 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
  764. return ( m_vecMapsBeingUpdated.Count() != 0 || m_bHostedCollectionUpdatePending );
  765. }
  766. void CDedicatedServerWorkshopManager::UpdateFile( PublishedFileId_t id )
  767. {
  768. if ( !m_FileInfoQueries.HasElement( id ) )
  769. m_FileInfoQueries.AddToTail( id );
  770. if ( !m_vecMapsBeingUpdated.HasElement( id ) )
  771. m_vecMapsBeingUpdated.AddToTail( id );
  772. }
  773. void CDedicatedServerWorkshopManager::UpdateFiles( const CUtlVector<PublishedFileId_t>& vecFileIDs )
  774. {
  775. FOR_EACH_VEC( vecFileIDs, i )
  776. {
  777. UpdateFile( vecFileIDs[i] );
  778. }
  779. }
  780. bool CDedicatedServerWorkshopManager::CurrentLevelNeedsUpdate( void ) const
  781. {
  782. return m_bCurrentLevelNeedsUpdate;
  783. }
  784. void CDedicatedServerWorkshopManager::CheckForNewVersion( PublishedFileId_t id )
  785. {
  786. bool bUpdateWorkshopCollectionToo = true;
  787. if ( m_fTimeLastVersionCheck != 0 && ( Plat_FloatTime() - m_fTimeLastVersionCheck < sv_ugc_manager_max_new_file_check_interval_secs.GetFloat() ) )
  788. {
  789. // If we don't have the map then we have to actually go and download it regardless of the timeout
  790. MapFileIdToUgcFileInfo_t::IndexType_t idxFileInfo = m_UGCFileInfos.Find( id );
  791. if ( ( idxFileInfo != m_UGCFileInfos.InvalidIndex() ) && m_UGCFileInfos.Element( idxFileInfo ) )
  792. {
  793. if ( sv_debug_ugc_downloads.GetBool() )
  794. 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) );
  795. return;
  796. }
  797. // We have recently checked the contents of workshop collection,
  798. // this time we are downloading some other map that users want
  799. // to play, so don't check collection
  800. bUpdateWorkshopCollectionToo = false;
  801. }
  802. UpdateFile( id );
  803. if ( !bUpdateWorkshopCollectionToo )
  804. return;
  805. // Remember last time we did full update
  806. m_fTimeLastVersionCheck = Plat_FloatTime();
  807. // check if the map collection changed
  808. if ( g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) )
  809. {
  810. PublishedFileId_t hostedCollectionID = V_atoui64( gpGlobals->mapGroupName.ToCStr() );
  811. // If collection's id is the map's id, then this is a mapgroup of one... no need to check for collection changes.
  812. if ( hostedCollectionID != id )
  813. {
  814. if ( !m_CollectionInfoQueries.HasElement( hostedCollectionID ) )
  815. m_CollectionInfoQueries.AddToTail( hostedCollectionID );
  816. m_bHostedCollectionUpdatePending = true;
  817. }
  818. }
  819. }
  820. const char* CDedicatedServerWorkshopManager::GetUGCMapPath( PublishedFileId_t id ) const
  821. {
  822. int idx = m_mapWorkshopIdsToMapNames.Find( id );
  823. if ( idx != m_mapWorkshopIdsToMapNames.InvalidIndex() )
  824. {
  825. return m_mapWorkshopIdsToMapNames[idx];
  826. }
  827. else
  828. {
  829. return NULL;
  830. }
  831. }
  832. PublishedFileId_t CDedicatedServerWorkshopManager::ParseCollectionInfo( KeyValues * pDetails )
  833. {
  834. int collection_detail_result = pDetails->GetInt( "result", 0 );
  835. EResult hResult = (EResult)collection_detail_result;
  836. KeyValues *pChildren = pDetails->FindKey( "children" );
  837. PublishedFileId_t ret = 0;
  838. if ( hResult == k_EResultOK && pChildren )
  839. {
  840. PublishedFileId_t collectionId = pDetails->GetUint64( "publishedfileid", 0 );
  841. if ( sv_debug_ugc_downloads.GetBool() )
  842. {
  843. Msg( "Received info for collection id %llu:\n", collectionId );
  844. }
  845. CUtlVector<PublishedFileId_t> vecCollectionIDs;
  846. for ( KeyValues *pFile = pChildren->GetFirstSubKey(); pFile != NULL; pFile = pFile->GetNextKey() )
  847. {
  848. PublishedFileId_t id = pFile->GetUint64( "publishedfileid" );
  849. vecCollectionIDs.AddToTail( id );
  850. if ( sv_debug_ugc_downloads.GetBool() )
  851. Msg( " file ID: %llu\n", id );
  852. }
  853. if ( ShouldUpdateCollection( collectionId, vecCollectionIDs ) )
  854. {
  855. UpdateFiles( vecCollectionIDs );
  856. OnCollectionInfoReceived( collectionId, vecCollectionIDs );
  857. }
  858. ret = collectionId;
  859. }
  860. return ret;
  861. }
  862. CStreamingUGCDownloader::CStreamingUGCDownloader():m_fileBuffer( 1024*1024, 1024*1024, 0 )
  863. {
  864. m_ioAsyncControl = NULL;
  865. m_unChunkSize = 0;
  866. m_unBytesReceived = 0;
  867. m_unFileSizeInBytes = 0;
  868. m_pFileInfo = NULL;
  869. m_bIsFinished = false;
  870. m_bHTTPRequestPending = false;
  871. m_flTimeLastMessage = 0.0f;
  872. }
  873. void CStreamingUGCDownloader::Cleanup( void )
  874. {
  875. if ( m_ioAsyncControl )
  876. {
  877. filesystem->AsyncAbort( m_ioAsyncControl );
  878. filesystem->AsyncFinish( m_ioAsyncControl, true );
  879. filesystem->AsyncRelease( m_ioAsyncControl );
  880. m_ioAsyncControl = NULL;
  881. }
  882. m_fileBuffer.Clear();
  883. if ( filesystem->FileExists( m_szTempFileName ) )
  884. {
  885. if ( sv_debug_ugc_downloads.GetBool() )
  886. Msg( "Clearing temp file(%s) for %llu : %s\n", m_szTempFileName, m_pFileInfo->fileId, m_pFileInfo->m_szFileName );
  887. filesystem->RemoveFile( m_szTempFileName );
  888. }
  889. if ( steamgameserverapicontext )
  890. {
  891. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  892. if ( pHTTP && m_bHTTPRequestPending )
  893. {
  894. if ( sv_debug_ugc_downloads.GetBool() )
  895. Msg( "Canceling download for %llu : %s\n", m_pFileInfo->fileId, m_pFileInfo->m_szFileName );
  896. pHTTP->ReleaseHTTPRequest( m_hReq );
  897. }
  898. }
  899. m_httpRequestCallback.Cancel();
  900. }
  901. CStreamingUGCDownloader::~CStreamingUGCDownloader()
  902. {
  903. Cleanup();
  904. }
  905. void CStreamingUGCDownloader::StartFileDownload( const DedicatedServerUGCFileInfo_t *pFileInfo, uint32 unChunkSize )
  906. {
  907. m_bIsFinished = false;
  908. V_snprintf( m_szTempFileName, ARRAYSIZE( m_szTempFileName ), "%s/%llu/%llu.tmp", g_szWorkshopMapBasePath, pFileInfo->fileId, pFileInfo->fileId );
  909. // Make sure target directory exists
  910. char buf[MAX_PATH];
  911. V_ExtractFilePath( m_szTempFileName, buf, sizeof( buf ) );
  912. g_pFullFileSystem->CreateDirHierarchy( buf, "DEFAULT_WRITE_PATH" );
  913. if ( filesystem->FileExists( m_szTempFileName, "MOD" ) )
  914. {
  915. filesystem->RemoveFile(m_szTempFileName, "MOD" );
  916. }
  917. m_pFileInfo = pFileInfo;
  918. m_unBytesReceived = 0;
  919. m_unFileSizeInBytes = pFileInfo->m_unFileSizeInBytes;
  920. m_unChunkSize = unChunkSize;
  921. m_fileBuffer.EnsureCapacity( m_unChunkSize );
  922. // Doing one download at a time-- don't start requesting content until it's this downloader's turn.
  923. // HTTPRequestPartialContent( 0, unChunkSize );
  924. V_strcpy_safe( m_szMapTitle, pFileInfo->m_szTitle );
  925. if ( sv_debug_ugc_downloads.GetBool() )
  926. {
  927. Msg( "Starting download for file id %llu:'%s'.\n", pFileInfo->fileId, pFileInfo->m_szTitle ? pFileInfo->m_szTitle : "<no title>" );
  928. }
  929. if ( sv_broadcast_ugc_downloads.GetBool() )
  930. {
  931. UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "Server: Downloading new map '%s', please wait...", pFileInfo->m_szTitle ) );
  932. m_flTimeLastMessage = gpGlobals->curtime;
  933. }
  934. }
  935. void CStreamingUGCDownloader::HTTPRequestPartialContent( uint32 rangeStart, uint32 rangeEnd )
  936. {
  937. Assert( steamgameserverapicontext );
  938. if ( steamgameserverapicontext == NULL )
  939. return;
  940. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  941. Assert( pHTTP );
  942. if ( !pHTTP )
  943. return;
  944. m_hReq = pHTTP->CreateHTTPRequest( k_EHTTPMethodGET, m_pFileInfo->m_szUrl );
  945. SteamAPICall_t hCall;
  946. CFmtStr byteRange( "bytes=%d-%d", rangeStart, rangeEnd );
  947. pHTTP->SetHTTPRequestHeaderValue( m_hReq, "range", byteRange.Access() );
  948. pHTTP->SendHTTPRequest( m_hReq, &hCall );
  949. m_httpRequestCallback.SetGameserverFlag();
  950. m_httpRequestCallback.Set( hCall, this, &CStreamingUGCDownloader::OnHTTPRequestComplete );
  951. m_bHTTPRequestPending = true;
  952. if ( sv_broadcast_ugc_downloads.GetBool() && gpGlobals->curtime - m_flTimeLastMessage > sv_broadcast_ugc_download_progress_interval.GetFloat() )
  953. {
  954. UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "Server: %.0f%% downloaded for '%s'...", ((float)rangeStart / (float)m_unFileSizeInBytes) * 100.0f, m_szMapTitle ) );
  955. m_flTimeLastMessage = gpGlobals->curtime;
  956. }
  957. }
  958. void CStreamingUGCDownloader::OnHTTPRequestComplete( HTTPRequestCompleted_t *arg, bool bFailed )
  959. {
  960. Assert( steamgameserverapicontext );
  961. if ( steamgameserverapicontext == NULL )
  962. return;
  963. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  964. Assert( pHTTP );
  965. if ( !pHTTP )
  966. return;
  967. Assert( arg );
  968. if ( !arg )
  969. return;
  970. if ( arg->m_eStatusCode == k_EHTTPStatusCode206PartialContent )
  971. {
  972. uint32 unBodySize;
  973. if ( pHTTP->GetHTTPResponseBodySize( arg->m_hRequest, &unBodySize ) )
  974. {
  975. if ( sv_debug_ugc_downloads.GetBool() )
  976. Msg( "Receiving bytes %u-%u for file %s (%s)\n", m_unBytesReceived, m_unBytesReceived + unBodySize, m_szTempFileName, m_pFileInfo->m_szFileName );
  977. m_unBytesReceived += unBodySize;
  978. m_fileBuffer.EnsureCapacity( unBodySize );
  979. m_fileBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, unBodySize );
  980. if ( pHTTP->GetHTTPResponseBodyData( arg->m_hRequest, (uint8*)m_fileBuffer.Base(), unBodySize ) )
  981. {
  982. filesystem->AsyncAppend( m_szTempFileName, (void*)m_fileBuffer.Base(), unBodySize, false, &m_ioAsyncControl );
  983. }
  984. }
  985. // todo FAIL-- abort
  986. }
  987. pHTTP->ReleaseHTTPRequest( arg->m_hRequest );
  988. m_bHTTPRequestPending = false;
  989. }
  990. void CStreamingUGCDownloader::Update( void )
  991. {
  992. Assert( steamgameserverapicontext );
  993. if ( steamgameserverapicontext == NULL )
  994. return;
  995. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  996. Assert( pHTTP );
  997. if ( !pHTTP )
  998. return;
  999. // Free to ask for more content if async write is done, or if we haven't started writing yet.
  1000. bool bDoneWriting = m_ioAsyncControl == NULL || filesystem->AsyncStatus( m_ioAsyncControl ) == FSASYNC_OK;
  1001. if ( bDoneWriting == true && m_bHTTPRequestPending == false )
  1002. {
  1003. if ( m_unBytesReceived < m_unFileSizeInBytes )
  1004. {
  1005. HTTPRequestPartialContent( m_unBytesReceived, MIN( m_unBytesReceived + m_unChunkSize, m_unFileSizeInBytes ) - 1 );
  1006. }
  1007. else
  1008. {
  1009. // remove the older file if it exists
  1010. //BUG: If we're running this map, the copy will fail. Defer in that case.
  1011. if ( filesystem->FileExists( m_pFileInfo->m_szFilePath ) )
  1012. {
  1013. filesystem->RemoveFile( m_pFileInfo->m_szFilePath );
  1014. }
  1015. // If authors rename the map file, old versions get orphaned in the workshop directory. Nuke any bsp here.
  1016. if ( sv_remove_old_ugc_downloads.GetBool() )
  1017. {
  1018. CUtlVector<CUtlString> outList;
  1019. AddFilesToList( outList, CFmtStr( "%s/%llu/", g_szWorkshopMapBasePath, m_pFileInfo->fileId ).Access(), "MOD", "bsp" );
  1020. FOR_EACH_VEC( outList, i )
  1021. {
  1022. filesystem->RemoveFile( outList[i] );
  1023. }
  1024. }
  1025. char szFullPathToTempFile[MAX_PATH];
  1026. g_pFullFileSystem->RelativePathToFullPath( m_szTempFileName, "MOD", szFullPathToTempFile, sizeof( szFullPathToTempFile ) );
  1027. if ( UnzipFile( szFullPathToTempFile ) == false )
  1028. {
  1029. // Not a zip file, just rename it
  1030. g_pFullFileSystem->RenameFile( m_szTempFileName, m_pFileInfo->m_szFilePath );
  1031. }
  1032. //
  1033. // Timestamp the file to match workshop updated timestamp
  1034. //
  1035. UGCUtil_TimestampFile( m_pFileInfo->m_szFilePath, m_pFileInfo->m_unTimeLastUpdated );
  1036. m_bIsFinished = true;
  1037. if ( 1 )//sv_debug_ugc_downloads.GetBool() )
  1038. {
  1039. 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 );
  1040. }
  1041. }
  1042. }
  1043. }
  1044. void CWorkshopMapGroupBuilder::OnMapDownloaded( const DedicatedServerUGCFileInfo_t *pInfo )
  1045. {
  1046. MapOnDisk( pInfo->fileId, pInfo->m_szFilePath );
  1047. }
  1048. CWorkshopMapGroupBuilder::CWorkshopMapGroupBuilder( PublishedFileId_t id, const CUtlVector< PublishedFileId_t >& mapFileIDs )
  1049. {
  1050. m_id = id;
  1051. m_pendingMapInfos.AddVectorToTail( mapFileIDs );
  1052. FOR_EACH_VEC( m_pendingMapInfos, i )
  1053. {
  1054. const char* szPath = DedicatedServerWorkshop().GetUGCMapPath( m_pendingMapInfos[i] );
  1055. if ( szPath )
  1056. {
  1057. MapOnDisk( m_pendingMapInfos[i], szPath );
  1058. }
  1059. }
  1060. }
  1061. void CWorkshopMapGroupBuilder::CreateOrUpdateMapGroup( void )
  1062. {
  1063. g_pGameTypes->CreateOrUpdateWorkshopMapGroup( CFmtStr( "%llu", m_id ).Access(), m_Maps );
  1064. }
  1065. const char* CWorkshopMapGroupBuilder::GetFirstMap( void ) const
  1066. {
  1067. return m_Maps.Count() > 0 ? m_Maps.Head() : NULL;
  1068. }
  1069. const char* CWorkshopMapGroupBuilder::GetMapMatchingId( PublishedFileId_t id ) const
  1070. {
  1071. FOR_EACH_VEC( m_Maps, i )
  1072. {
  1073. if ( id == GetMapIDFromMapPath( m_Maps[i] ) )
  1074. return m_Maps[i];
  1075. }
  1076. return GetFirstMap();
  1077. }
  1078. void CWorkshopMapGroupBuilder::RemoveRequiredMap( PublishedFileId_t id )
  1079. {
  1080. m_pendingMapInfos.FindAndFastRemove( id );
  1081. }
  1082. void CWorkshopMapGroupBuilder::MapOnDisk( PublishedFileId_t id, const char* szPath )
  1083. {
  1084. int idx = m_pendingMapInfos.Find( id );
  1085. if ( idx != m_pendingMapInfos.InvalidIndex() )
  1086. {
  1087. m_pendingMapInfos.FastRemove( idx );
  1088. // Build path to the map file without any extensions and without the 'maps' dir in the path (maps dir is assumed by other systems).
  1089. char szMapPath[MAX_PATH];
  1090. char szInputPath[MAX_PATH];
  1091. V_strcpy_safe( szInputPath, szPath );
  1092. V_FixSlashes( szInputPath, '/' );
  1093. const char* szMapsPrefix = "maps/";
  1094. if ( V_stristr( szInputPath, szMapsPrefix ) == szInputPath )
  1095. {
  1096. V_strcpy_safe( szMapPath, szInputPath + strlen( szMapsPrefix ) );
  1097. }
  1098. V_StripExtension( szMapPath, szMapPath, sizeof( szMapPath ) );
  1099. m_Maps.CopyAndAddToTail( szMapPath ); // CUtlStringList auto purges on destruct
  1100. }
  1101. }
  1102. CBaseWorkshopHTTPRequest::CBaseWorkshopHTTPRequest( const CUtlVector<PublishedFileId_t> &vecFileIDs )
  1103. {
  1104. m_handle = INVALID_HTTPREQUEST_HANDLE;
  1105. m_bFinished = false;
  1106. m_vecItemsQueried.AddVectorToTail( vecFileIDs );
  1107. }
  1108. CBaseWorkshopHTTPRequest::~CBaseWorkshopHTTPRequest()
  1109. {
  1110. if ( steamgameserverapicontext )
  1111. {
  1112. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  1113. if ( pHTTP && m_handle != INVALID_HTTPREQUEST_HANDLE )
  1114. {
  1115. pHTTP->ReleaseHTTPRequest( m_handle );
  1116. }
  1117. }
  1118. m_httpCallback.Cancel();
  1119. }
  1120. void CBaseWorkshopHTTPRequest::OnHTTPRequestComplete( HTTPRequestCompleted_t *arg, bool bFailed )
  1121. {
  1122. m_bFinished = true;
  1123. m_lastHTTPResult = arg->m_eStatusCode;
  1124. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  1125. if ( arg->m_bRequestSuccessful == false )
  1126. {
  1127. Warning( "Server UGC Manager: Failed to get file info. Internal IHTTP error or clientside internet connection problem." );
  1128. }
  1129. else if ( arg->m_eStatusCode != k_EHTTPStatusCode200OK || sv_test_steam_connection_failure.GetBool() )
  1130. {
  1131. Warning( "Server UGC Manager: Failed to get file info. HTTP status %d \n", arg->m_eStatusCode );
  1132. }
  1133. else
  1134. {
  1135. uint32 unBodySize;
  1136. if ( !pHTTP->GetHTTPResponseBodySize( arg->m_hRequest, &unBodySize ) )
  1137. {
  1138. Assert( 0 );
  1139. Warning( "Server UGC Manager: GetHTTPResponseBodySize failed\n" );
  1140. }
  1141. else
  1142. {
  1143. if ( sv_debug_ugc_downloads.GetBool() )
  1144. Msg( "Fetched %d bytes via HTTP:\n", unBodySize );
  1145. if ( unBodySize > 0 )
  1146. {
  1147. CUtlBuffer resBuffer( 0, unBodySize, 0 );
  1148. resBuffer.SetBufferType( true, true );
  1149. resBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, unBodySize );
  1150. pHTTP->GetHTTPResponseBodyData( arg->m_hRequest, (uint8*)resBuffer.Base(), resBuffer.TellPut() );
  1151. KeyValues *pResponseKV = new KeyValues("");
  1152. pResponseKV->UsesEscapeSequences( true );
  1153. KeyValuesAD autodelete( pResponseKV );
  1154. bool bLoadSucessful = pResponseKV->LoadFromBuffer( NULL, resBuffer );
  1155. if ( sv_debug_ugc_downloads.GetBool() )
  1156. KeyValuesDumpAsDevMsg( pResponseKV, 1 );
  1157. if ( !bLoadSucessful )
  1158. Msg( "CDedicatedServerWorkshopManager: Failed to load http result to KV buffer\n" );
  1159. if ( bLoadSucessful )
  1160. {
  1161. ProcessHTTPResponse( pResponseKV );
  1162. }
  1163. }
  1164. }
  1165. }
  1166. pHTTP->ReleaseHTTPRequest( arg->m_hRequest );
  1167. }
  1168. CPublishedFileInfoHTTPRequest::CPublishedFileInfoHTTPRequest( const CUtlVector<PublishedFileId_t>& vecFileIDs )
  1169. : CBaseWorkshopHTTPRequest( vecFileIDs )
  1170. {
  1171. }
  1172. CPublishedFileInfoHTTPRequest::~CPublishedFileInfoHTTPRequest()
  1173. {
  1174. m_vecFileInfos.PurgeAndDeleteElements();
  1175. }
  1176. HTTPRequestHandle CPublishedFileInfoHTTPRequest::CreateHTTPRequest( const char* szAuthKey )
  1177. {
  1178. if ( steamgameserverapicontext && steamgameserverapicontext->SteamHTTP() )
  1179. {
  1180. CFmtStr strItemCount( "%d", m_vecItemsQueried.Count() );
  1181. const char* szUrl = CFmtStr("%s/Service/PublishedFile/GetDetails/v1/", GetApiBaseUrl()).Access();
  1182. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  1183. m_handle = pHTTP->CreateHTTPRequest( k_EHTTPMethodGET, szUrl );
  1184. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "format", "vdf" );
  1185. FOR_EACH_VEC( m_vecItemsQueried, i )
  1186. {
  1187. CFmtStr entry( "publishedfileids[%d]", i );
  1188. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, entry.Access(), CFmtStr("%llu", m_vecItemsQueried[i] ).Access() );
  1189. }
  1190. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "key", szAuthKey );
  1191. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "minimal_details", "1" );
  1192. SteamAPICall_t hCall;
  1193. pHTTP->SendHTTPRequest( m_handle, &hCall);
  1194. m_httpCallback.SetGameserverFlag();
  1195. m_httpCallback.Set( hCall, this, &CBaseWorkshopHTTPRequest::OnHTTPRequestComplete );
  1196. }
  1197. else
  1198. {
  1199. m_bFinished = true;
  1200. }
  1201. return m_handle;
  1202. }
  1203. void CPublishedFileInfoHTTPRequest::ProcessHTTPResponse( KeyValues *pResponseKV )
  1204. {
  1205. KeyValues *pPublishedFileDetails = pResponseKV->FindKey( "publishedfiledetails", false );
  1206. if ( pPublishedFileDetails )
  1207. {
  1208. for ( KeyValues *fileDetails = pPublishedFileDetails->GetFirstSubKey(); fileDetails != NULL; fileDetails = fileDetails->GetNextKey() )
  1209. {
  1210. DedicatedServerUGCFileInfo_t * pNewFileInfo = new DedicatedServerUGCFileInfo_t;
  1211. pNewFileInfo->BuildFromKV( fileDetails );
  1212. m_vecFileInfos.AddToTail( pNewFileInfo );
  1213. }
  1214. }
  1215. }
  1216. CCollectionInfoHTTPRequest::CCollectionInfoHTTPRequest( const CUtlVector<PublishedFileId_t>& vecFileIDs )
  1217. : CBaseWorkshopHTTPRequest( vecFileIDs )
  1218. {
  1219. m_pResponseKV = NULL;
  1220. }
  1221. CCollectionInfoHTTPRequest::~CCollectionInfoHTTPRequest()
  1222. {
  1223. if ( m_pResponseKV )
  1224. {
  1225. m_pResponseKV->deleteThis();
  1226. m_pResponseKV = NULL;
  1227. }
  1228. }
  1229. HTTPRequestHandle CCollectionInfoHTTPRequest::CreateHTTPRequest( const char* szAuthKey /*= NULL */ )
  1230. {
  1231. if ( steamgameserverapicontext && steamgameserverapicontext->SteamHTTP() )
  1232. {
  1233. CFmtStr strItemCount( "%d", m_vecItemsQueried.Count() );
  1234. const char* szUrl = CFmtStr("%s/ISteamRemoteStorage/GetCollectionDetails/v0001/", GetApiBaseUrl()).Access();
  1235. ISteamHTTP *pHTTP = steamgameserverapicontext->SteamHTTP();
  1236. m_handle = pHTTP->CreateHTTPRequest( k_EHTTPMethodPOST, szUrl );
  1237. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "format", "vdf" );
  1238. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "collectioncount", strItemCount.Access() );
  1239. FOR_EACH_VEC( m_vecItemsQueried, i )
  1240. {
  1241. CFmtStr entry( "publishedfileids[%d]", i );
  1242. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, entry.Access(), CFmtStr("%llu", m_vecItemsQueried[i] ).Access() );
  1243. }
  1244. pHTTP->SetHTTPRequestGetOrPostParameter( m_handle, "key", szAuthKey );
  1245. SteamAPICall_t hCall;
  1246. pHTTP->SendHTTPRequest( m_handle, &hCall);
  1247. m_httpCallback.SetGameserverFlag();
  1248. m_httpCallback.Set( hCall, this, &CBaseWorkshopHTTPRequest::OnHTTPRequestComplete );
  1249. }
  1250. else
  1251. {
  1252. m_bFinished = true;
  1253. }
  1254. return m_handle;
  1255. }
  1256. void CCollectionInfoHTTPRequest::ProcessHTTPResponse( KeyValues *pResponseKV )
  1257. {
  1258. KeyValues *pCollectionDetails = pResponseKV->FindKey( "collectiondetails" );
  1259. if ( pCollectionDetails )
  1260. {
  1261. Assert( m_pResponseKV == NULL );
  1262. if ( m_pResponseKV )
  1263. m_pResponseKV->deleteThis();
  1264. m_pResponseKV = pCollectionDetails->MakeCopy();
  1265. }
  1266. else
  1267. {
  1268. Assert( 0 );
  1269. Msg( "CCollectionInfoHTTPRequest: Could not parse response for collection info\n" );
  1270. }
  1271. }