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.

1095 lines
34 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Utility helper functions for dealing with UGC files
  4. //
  5. //==========================================================================//
  6. #include "cbase.h"
  7. #include "ugc_utils.h"
  8. #include "logging.h"
  9. #include "globalvars_base.h"
  10. #include "strtools.h"
  11. #ifdef CLIENT_DLL
  12. #include "imageutils.h"
  13. #endif
  14. #include "zip/XUnzip.h"
  15. #include "vstdlib/jobthread.h"
  16. #include "tier2/fileutils.h"
  17. #if defined( _WIN32 )
  18. #include <sys/utime.h>
  19. #elif defined(OSX)
  20. #include <utime.h>
  21. #else
  22. #include <sys/types.h>
  23. #include <utime.h>
  24. #endif
  25. // FIXME: These need to be properly arranged to make this viable!
  26. extern IFileSystem *filesystem;
  27. #define FILEREQUEST_IO_STALL_DELAY 30.0f // Seconds
  28. #define DOWNLOAD_CHUNK_SIZE 10485760 // 10MB
  29. #define THUMBNAIL_SMALL_WIDTH 256
  30. #define THUMBNAIL_SMALL_HEIGHT 144
  31. #if !defined( NO_STEAM )
  32. extern CSteamAPIContext *steamapicontext; // available on game clients
  33. #endif // !NO_STEAM
  34. #if !defined( NO_STEAM ) && !defined ( _PS3 )
  35. Color g_WorkshopLogColor( 0, 255, 255, 255 );
  36. BEGIN_DEFINE_LOGGING_CHANNEL( LOG_WORKSHOP, "Workshop", LCF_CONSOLE_ONLY, LS_WARNING, g_WorkshopLogColor );
  37. ADD_LOGGING_CHANNEL_TAG( "UGCOperation" );
  38. ADD_LOGGING_CHANNEL_TAG( "WorkshopOperation" );
  39. END_DEFINE_LOGGING_CHANNEL();
  40. ConVar cl_remove_old_ugc_downloads( "cl_remove_old_ugc_downloads", "1", FCVAR_RELEASE );
  41. //-----------------------------------------------------------------------------
  42. // Purpose: Helper function for Steam's remote storage interface
  43. //-----------------------------------------------------------------------------
  44. ISteamRemoteStorage *GetISteamRemoteStorage()
  45. {
  46. return ( steamapicontext != NULL ) ? steamapicontext->SteamRemoteStorage() : NULL;
  47. }
  48. //-----------------------------------------------------------------------------
  49. // Purpose: Helper function to get the map Published File ID from a map path
  50. //-----------------------------------------------------------------------------
  51. PublishedFileId_t GetMapIDFromMapPath( const char *pMapPath )
  52. {
  53. char tmp[MAX_PATH];
  54. V_strcpy_safe( tmp, pMapPath );
  55. V_FixSlashes( tmp, '/' ); // internal path strings use forward slashes, make sure we compare like that.
  56. if ( V_strstr( tmp, "workshop/" ) )
  57. {
  58. V_StripFilename(tmp);
  59. V_StripTrailingSlash(tmp);
  60. const char* szDirName = V_GetFileName(tmp);
  61. return (PublishedFileId_t)V_atoui64(szDirName);
  62. }
  63. return 0;
  64. }
  65. bool UGCUtil_TimestampFile( const char *pFileRelativePath, uint32 uTimestamp )
  66. {
  67. char chFullFilePathForTimestamp[ MAX_PATH ] = {0};
  68. if ( char const *pchFullPath = g_pFullFileSystem->RelativePathToFullPath( pFileRelativePath, "MOD", chFullFilePathForTimestamp, sizeof( chFullFilePathForTimestamp ) ) )
  69. {
  70. struct utimbuf tbuffer;
  71. tbuffer.modtime = tbuffer.actime = uTimestamp;
  72. int iResultCode = utime( pchFullPath, &tbuffer );
  73. // There is an inconsistency between what utime writes and what stat returns due to daylight savings.
  74. // Check if what we wrote is being offset, then re-set the time to make it match what steam has recorded for last modify time.
  75. uint32 unFileTimeFromStat = (uint32)g_pFullFileSystem->GetFileTime( pFileRelativePath, "MOD" );
  76. if ( unFileTimeFromStat != uTimestamp )
  77. {
  78. int32 nDLSOffset = unFileTimeFromStat - uTimestamp;
  79. tbuffer.modtime = tbuffer.actime = uTimestamp - nDLSOffset;
  80. iResultCode = utime( pchFullPath, &tbuffer );
  81. #if defined ( DEBUG )
  82. unFileTimeFromStat = (uint32)g_pFullFileSystem->GetFileTime( pFileRelativePath, "MOD" );
  83. Assert( unFileTimeFromStat == uTimestamp );
  84. #endif
  85. }
  86. return ( iResultCode == 0 );
  87. }
  88. return false;
  89. }
  90. inline bool IsZip( void *z )
  91. {
  92. return ( z && *(unsigned int *)z == 0x04034b50 );
  93. }
  94. //-----------------------------------------------------------------------------
  95. // CUGCUnzipper
  96. //-----------------------------------------------------------------------------
  97. class CUGCUnzipJob : public CJob
  98. {
  99. public:
  100. CUGCUnzipJob( const char *szTargetFile, const char *szTempFile );
  101. virtual JobStatus_t DoExecute();
  102. bool IsFinished ( void ) const { return m_bIsFinished; }
  103. private:
  104. bool m_bIsFinished;
  105. char m_szTargetFile[MAX_PATH]; // Game dir relative path for unzipped file
  106. char m_szTempFile[MAX_PATH];
  107. };
  108. CUGCUnzipJob::CUGCUnzipJob( const char *szTargetFile, const char *szTempFile )
  109. {
  110. V_strcpy_safe( m_szTargetFile, szTargetFile );
  111. V_strcpy_safe( m_szTempFile, szTempFile );
  112. m_bIsFinished = false;
  113. }
  114. JobStatus_t CUGCUnzipJob::DoExecute()
  115. {
  116. CUtlBuffer unzipBuf;
  117. const uint32 unUnzipBufSize = 10 * 1024 * 1024;
  118. unzipBuf.EnsureCapacity( unUnzipBufSize );
  119. if ( HZIP hz = OpenZip( m_szTempFile, 0, ZIP_FILENAME ) )
  120. {
  121. ZIPENTRY ze;
  122. ZRESULT zr = GetZipItem( hz, -1, &ze ); // get count
  123. if ( zr == ZR_OK )
  124. {
  125. Assert( ze.index == 1 ); // This code assumes there is just a single file in the zip.
  126. if ( ZR_OK == GetZipItem( hz, 0, &ze ) )
  127. {
  128. FileHandle_t fh = g_pFullFileSystem->Open( m_szTargetFile, "wb", "MOD" );
  129. uint32 unBytesWritten = 0;
  130. do
  131. {
  132. zr = UnzipItem( hz, 0, unzipBuf.Base(), unUnzipBufSize, ZIP_MEMORY );
  133. uint32 unBytesToWrite = MIN( unUnzipBufSize, ze.unc_size - unBytesWritten );
  134. if ( unBytesToWrite > 0 )
  135. unBytesWritten += g_pFullFileSystem->Write( unzipBuf.Base(), unBytesToWrite, fh );
  136. } while ( zr == ZR_MORE );
  137. if ( zr != ZR_OK )
  138. {
  139. char errorBuf[256];
  140. FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) );
  141. Warning( "Failed unzipping entry '%s'. Reason: %s \n", ze.name, errorBuf );
  142. }
  143. g_pFullFileSystem->Close( fh );
  144. }
  145. }
  146. else
  147. {
  148. char errorBuf[256];
  149. FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) );
  150. Warning( "Failed to get count of items. Reason: %s \n", errorBuf );
  151. }
  152. CloseZip( hz );
  153. }
  154. m_bIsFinished = true;
  155. return JOB_OK;
  156. }
  157. IThreadPool *g_pUGCUnzipThreadPool = NULL;
  158. void UGCUtil_Shutdown()
  159. {
  160. if ( g_pUGCUnzipThreadPool )
  161. {
  162. g_pUGCUnzipThreadPool->Stop();
  163. DestroyThreadPool( g_pUGCUnzipThreadPool );
  164. g_pUGCUnzipThreadPool = NULL;
  165. }
  166. }
  167. void UGCUtil_Init()
  168. {
  169. Assert( g_pUGCUnzipThreadPool == NULL );
  170. if ( !g_pUGCUnzipThreadPool )
  171. {
  172. ThreadPoolStartParams_t params;
  173. params.nThreads = 1;
  174. params.nStackSize = 1024*1024;
  175. params.fDistribute = TRS_FALSE;
  176. g_pUGCUnzipThreadPool= CreateNewThreadPool();
  177. g_pUGCUnzipThreadPool->Start( params, "UGCUnzipThreadPool" );
  178. }
  179. }
  180. //-----------------------------------------------------------------------------
  181. // Constructor
  182. //-----------------------------------------------------------------------------
  183. CUGCFileRequest::CUGCFileRequest( void ) :
  184. m_hCloudID( k_UGCHandleInvalid ),
  185. m_UGCStatus( UGCFILEREQUEST_READY ),
  186. m_AsyncControl( NULL ),
  187. m_flIOStartTime( 0 ),
  188. m_flDownloadProgress( 0.0f ),
  189. m_tFileUpdateTime( 0 ),
  190. m_pUnzipJob( NULL )
  191. {
  192. // Start with these disabled
  193. m_szFileName[0] = '\0';
  194. m_szTargetDirectory[0] = '\0';
  195. m_szTargetFilename[0] = '\0';
  196. m_szErrorText[0] = '\0';
  197. #ifdef FILEREQUEST_IO_STALL
  198. m_nIOStallType = FILEREQUEST_STALL_DOWNLOAD;//FILEREQUEST_STALL_WRITE;
  199. m_flIOStallDuration = FILEREQUEST_IO_STALL_DELAY; // seconds
  200. #endif // FILEREQUEST_IO_STALL
  201. }
  202. //-----------------------------------------------------------------------------
  203. // Destructor
  204. //-----------------------------------------------------------------------------
  205. CUGCFileRequest::~CUGCFileRequest( void )
  206. {
  207. // Finish the file i/o
  208. if ( m_AsyncControl != NULL )
  209. {
  210. g_pFullFileSystem->AsyncFinish( m_AsyncControl );
  211. g_pFullFileSystem->AsyncRelease( m_AsyncControl );
  212. m_AsyncControl = NULL;
  213. }
  214. // Clear our internal buffer
  215. m_bufContents.Purge();
  216. }
  217. //-----------------------------------------------------------------------------
  218. // Purpose: Check if the file is in sync with the cloud
  219. //-----------------------------------------------------------------------------
  220. bool CUGCFileRequest::FileInSync( const char *lpszTargetDirectory, const char *lpszTargetFilename, uint32 timeUpdated )
  221. {
  222. if ( lpszTargetFilename == NULL )
  223. return false;
  224. char chCheckTargetDirectory[MAX_PATH] = {0};
  225. V_strncpy( chCheckTargetDirectory, lpszTargetDirectory, sizeof( chCheckTargetDirectory ) );
  226. V_FixSlashes( chCheckTargetDirectory, '/' );
  227. if ( const char *pszWorkshopMapId = StringAfterPrefix( chCheckTargetDirectory, "maps/workshop/" ) )
  228. {
  229. PublishedFileId_t uiWorkshopMapId = Q_atoui64( pszWorkshopMapId );
  230. if ( UGCUtil_IsOfficialMap( uiWorkshopMapId ) )
  231. return true;
  232. }
  233. #ifdef FILEREQUEST_IO_STALL
  234. return false;
  235. #endif // FILEREQUEST_IO_STALL
  236. char szFilename[MAX_PATH];
  237. V_SafeComposeFilename( lpszTargetDirectory, lpszTargetFilename, szFilename, ARRAYSIZE(szFilename) );
  238. // If the file exists, we need to check it's information
  239. if ( g_pFullFileSystem->FileExists( szFilename ) )
  240. {
  241. if ( timeUpdated != 0 )
  242. {
  243. // mtime needs to match the time last updated exactly, as we slam the file time when we download
  244. // so an earlier time is out of date and a later time may be modified due to file copying.
  245. uint32 fileTime = (uint32) g_pFullFileSystem->GetFileTime( szFilename, "MOD" );
  246. if ( timeUpdated == fileTime )
  247. return true;
  248. }
  249. else
  250. {
  251. // We didn't supply a time to check against, so we only cared about its existence
  252. return true;
  253. }
  254. }
  255. return false;
  256. }
  257. //-----------------------------------------------------------------------------
  258. // Purpose: Start a download by handle
  259. //-----------------------------------------------------------------------------
  260. UGCFileRequestStatus_t CUGCFileRequest::StartDownload( UGCHandle_t hFileHandle,
  261. const char *lpszTargetDirectory /*= NULL*/,
  262. const char *lpszTargetFilename /*= NULL*/,
  263. uint32 timeUpdated /*=0*/,
  264. bool bForceUpdate /*=false*/ )
  265. {
  266. // Start with the assumption of failure
  267. m_UGCStatus = UGCFILEREQUEST_ERROR;
  268. m_tFileUpdateTime = timeUpdated;
  269. // First, see if this file is already down on the disk (unless we're overriding the call)
  270. if ( bForceUpdate == false && FileInSync( lpszTargetDirectory, lpszTargetFilename, timeUpdated ) )
  271. {
  272. m_hCloudID = hFileHandle;
  273. // Take a target directory for the file
  274. if ( lpszTargetDirectory != NULL )
  275. {
  276. V_strncpy( m_szTargetDirectory, lpszTargetDirectory, MAX_PATH );
  277. V_FixSlashes( m_szTargetDirectory );
  278. }
  279. // Take a target filename for the file
  280. if ( lpszTargetFilename != NULL )
  281. {
  282. V_strncpy( m_szTargetFilename, lpszTargetFilename, MAX_PATH );
  283. }
  284. #ifdef LOG_FILEREQUEST_PROGRESS
  285. Log_Msg( LOG_WORKSHOP, "[UGC] File %s%c%s already in sync on client. (Duration: %f seconds)\n", lpszTargetDirectory, CORRECT_PATH_SEPARATOR, lpszTargetFilename, gpGlobals->realtime-m_flIOStartTime );
  286. #endif // LOG_FILEREQUEST_PROGRESS
  287. MarkCompleteAndFree( false );
  288. return m_UGCStatus;
  289. }
  290. #ifdef LOG_FILEREQUEST_PROGRESS
  291. Log_Msg( LOG_WORKSHOP, "[UGC] Beginning download of %s%c%s. (%f)\n", lpszTargetDirectory, CORRECT_PATH_SEPARATOR, lpszTargetFilename, gpGlobals->realtime );
  292. #endif // LOG_FILEREQUEST_PROGRESS
  293. // Start the download request
  294. uint32 nPriority = 0; // FIXME: For now, we always download at an equal priority
  295. SteamAPICall_t hSteamAPICall = GetISteamRemoteStorage()->UGCDownload( hFileHandle, nPriority );
  296. m_callbackUGCDownload.Set( hSteamAPICall, this, &CUGCFileRequest::Steam_OnUGCDownload );
  297. if ( hSteamAPICall != k_uAPICallInvalid )
  298. {
  299. // Mark download as in progress
  300. m_UGCStatus = UGCFILEREQUEST_DOWNLOADING;
  301. m_hCloudID = hFileHandle;
  302. m_flIOStartTime = gpGlobals->realtime;
  303. // Take a target directory for the file
  304. if ( lpszTargetDirectory != NULL )
  305. {
  306. V_strncpy( m_szTargetDirectory, lpszTargetDirectory, MAX_PATH );
  307. V_FixSlashes( m_szTargetDirectory );
  308. }
  309. // Take a target filename for the file
  310. if ( lpszTargetFilename != NULL )
  311. {
  312. V_strncpy( m_szTargetFilename, lpszTargetFilename, MAX_PATH );
  313. }
  314. #ifdef FILEREQUEST_IO_STALL
  315. m_flIOStallStart = gpGlobals->realtime;
  316. #endif // FILEREQUEST_IO_STALL
  317. // Start with an initialized value for our progress
  318. m_flDownloadProgress = 0.0f;
  319. // Done!
  320. return m_UGCStatus;
  321. }
  322. // We were unable to start our download through the Steam API
  323. return ThrowError( "[UGC] Failed to initiate download of file (%s%s%s) from cloud\n", lpszTargetDirectory, CORRECT_PATH_SEPARATOR, lpszTargetFilename );
  324. }
  325. //-----------------------------------------------------------------------------
  326. // Purpose: Start an upload of a buffer by filename
  327. //-----------------------------------------------------------------------------
  328. UGCFileRequestStatus_t CUGCFileRequest::StartUpload( CUtlBuffer &buffer, const char *lpszFilename )
  329. {
  330. // Start with the assumption of failure
  331. m_UGCStatus = UGCFILEREQUEST_ERROR;
  332. #ifdef LOG_FILEREQUEST_PROGRESS
  333. Log_Msg( LOG_WORKSHOP, "[UGC] Saving %s to user cloud. (%f)\n", lpszFilename, gpGlobals->realtime );
  334. #endif // LOG_FILEREQUEST_PROGRESS
  335. // Write the local copy of the file
  336. ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
  337. if ( pRemoteStorage == NULL )
  338. return ThrowError( "[UGC] Failed to write file to cloud\n" );
  339. UGCFileWriteStreamHandle_t hWriteStream = pRemoteStorage->FileWriteStreamOpen( lpszFilename );
  340. if ( hWriteStream == k_UGCFileStreamHandleInvalid )
  341. return ThrowError( "[UGC] Failed to write file to cloud\n" );
  342. uint32 nBytesWritten = 0;
  343. uint32 nBytesToWrite = buffer.TellPut();
  344. while ( nBytesToWrite )
  345. {
  346. uint32 nChunkSize = MIN( nBytesToWrite, (100*1024*1024) ); // 100Mb limit
  347. if ( !pRemoteStorage->FileWriteStreamWriteChunk( hWriteStream, buffer.PeekGet(nBytesWritten), nChunkSize ) )
  348. {
  349. // NOTE: This won't be necessary in future updates
  350. pRemoteStorage->FileWriteStreamCancel( hWriteStream );
  351. return ThrowError( "[UGC] Failed to write file to cloud\n" );
  352. }
  353. // Decrement the amount of bytes remaining
  354. nBytesToWrite -= nChunkSize;
  355. }
  356. if ( !pRemoteStorage->FileWriteStreamClose( hWriteStream ) )
  357. {
  358. return ThrowError( "[UGC] Failed to write file to cloud\n" );
  359. }
  360. #ifdef LOG_FILEREQUEST_PROGRESS
  361. Log_Msg( LOG_WORKSHOP, "[UGC] Sharing %s to user cloud. (%f)\n", lpszFilename, gpGlobals->realtime );
  362. #endif // LOG_FILEREQUEST_PROGRESS
  363. // Now share the file (uploads it to the cloud)
  364. SteamAPICall_t hSteamAPICall = pRemoteStorage->FileShare( lpszFilename );
  365. m_callbackFileShare.Set( hSteamAPICall, this, &CUGCFileRequest::Steam_OnFileShare );
  366. #ifdef FILEREQUEST_IO_STALL
  367. m_flIOStallStart = gpGlobals->realtime;
  368. #endif // FILEREQUEST_IO_STALL
  369. // Now, hold onto the filename
  370. V_ExtractFilePath( lpszFilename, m_szTargetDirectory, ARRAYSIZE( m_szTargetDirectory ) );
  371. V_StripTrailingSlash( m_szTargetDirectory );
  372. V_FixSlashes( m_szTargetDirectory );
  373. V_strncpy( m_szTargetFilename, V_UnqualifiedFileName( lpszFilename ), ARRAYSIZE( m_szTargetFilename ) );
  374. m_UGCStatus = UGCFILEREQUEST_UPLOADING;
  375. m_flIOStartTime = gpGlobals->realtime;
  376. return m_UGCStatus;
  377. }
  378. //-----------------------------------------------------------------------------
  379. // Purpose: FileShare complete for a file request
  380. //-----------------------------------------------------------------------------
  381. void CUGCFileRequest::Steam_OnFileShare( RemoteStorageFileShareResult_t *pResult, bool bError )
  382. {
  383. char szFilename[MAX_PATH];
  384. GetFullPath( szFilename, ARRAYSIZE(szFilename) );
  385. if ( bError )
  386. {
  387. ThrowError( "[UGC] Upload of file %s to Steam cloud failed!\n", szFilename );
  388. return;
  389. }
  390. #ifdef LOG_FILEREQUEST_PROGRESS
  391. Log_Msg( LOG_WORKSHOP, "[UGC] File %s shared to user cloud. UGC ID: %llu (%f)\n", szFilename, pResult->m_hFile, gpGlobals->realtime );
  392. #endif // LOG_FILEREQUEST_PROGRESS
  393. // Save the return handle
  394. m_hCloudID = pResult->m_hFile;
  395. MarkCompleteAndFree();
  396. }
  397. //-----------------------------------------------------------------------------
  398. // Purpose: UGDownload complete for a file request
  399. //-----------------------------------------------------------------------------
  400. void CUGCFileRequest::Steam_OnUGCDownload( RemoteStorageDownloadUGCResult_t *pResult, bool bError )
  401. {
  402. // Completed. Did we succeed?
  403. if ( bError || pResult->m_eResult != k_EResultOK )
  404. {
  405. ThrowError( "[UGC] Download of file %s from cloud failed with result %d (%llu)\n", pResult->m_pchFileName, pResult->m_eResult, m_hCloudID );
  406. return;
  407. }
  408. // Make sure we got back the file we were expecting
  409. Assert( pResult->m_hFile == m_hCloudID );
  410. // Fetch file details
  411. AppId_t nAppID;
  412. char *pchName;
  413. int32 nFileSizeInBytes = -1;
  414. CSteamID steamIDOwner;
  415. ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
  416. if ( !pRemoteStorage->GetUGCDetails( m_hCloudID, &nAppID, &pchName, &nFileSizeInBytes, &steamIDOwner ) || nFileSizeInBytes <= 0 )
  417. {
  418. ThrowError( "[UGC] Unable to retrieve cloud file %s (%llu) info from Steam\n", pResult->m_pchFileName, pResult->m_hFile );
  419. return;
  420. }
  421. // Save our name
  422. V_strncpy( m_szFileName, pchName, sizeof(m_szFileName) );
  423. bool bBSPFile = false;
  424. if ( V_strnicmp( V_GetFileExtensionSafe(m_szFileName), "bsp", 3 ) == 0 )
  425. {
  426. bBSPFile = true;
  427. }
  428. // Take this as our target if we haven't specified one
  429. if ( m_szTargetFilename[0] == '\0' )
  430. {
  431. V_strncpy( m_szTargetFilename, V_GetFileName( pchName ), sizeof(m_szTargetFilename) );
  432. }
  433. #ifdef LOG_FILEREQUEST_PROGRESS
  434. Log_Msg( LOG_WORKSHOP, "[UGC] Read cloud file %s%c%s (%llu) (%f)\n", m_szTargetDirectory, CORRECT_PATH_SEPARATOR, m_szTargetFilename, m_hCloudID, gpGlobals->realtime );
  435. #endif // LOG_FILEREQUEST_PROGRESS
  436. char szLocalFullPath[MAX_PATH];
  437. GetFullPath( szLocalFullPath, sizeof(szLocalFullPath) );
  438. // Make sure the directory exists if we're creating one
  439. if ( m_szTargetDirectory != NULL )
  440. {
  441. filesystem->CreateDirHierarchy( m_szTargetDirectory, "MOD" );
  442. }
  443. else
  444. {
  445. char szDirectory[MAX_PATH];
  446. Q_FileBase( szLocalFullPath, szDirectory, sizeof(szDirectory) );
  447. filesystem->CreateDirHierarchy( szDirectory, "MOD" );
  448. }
  449. // Allocate a temporary buffer
  450. m_bufContents.Purge();
  451. if ( bBSPFile )
  452. {
  453. m_bufContents.EnsureCapacity( DOWNLOAD_CHUNK_SIZE );
  454. m_bufContents.SeekPut( CUtlBuffer::SEEK_HEAD, DOWNLOAD_CHUNK_SIZE );
  455. }
  456. else
  457. {
  458. m_bufContents.EnsureCapacity( nFileSizeInBytes );
  459. m_bufContents.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSizeInBytes );
  460. }
  461. // Read in the data and save to tmp file
  462. FileHandle_t fh = NULL;
  463. char szZipFullPath[ MAX_PATH ];
  464. if ( bBSPFile )
  465. {
  466. GetFullPath( szZipFullPath, sizeof(szZipFullPath) );
  467. V_SetExtension( szZipFullPath, ".zip", MAX_PATH );
  468. fh = g_pFullFileSystem->Open( szZipFullPath, "wb", "MOD" );
  469. }
  470. bool bZipFile = false;
  471. uint32 nOffset = 0;
  472. while ( (int32) nOffset < nFileSizeInBytes )
  473. {
  474. uint32 nChunkSize = MIN( (nFileSizeInBytes-nOffset), DOWNLOAD_CHUNK_SIZE ); // 10Mb
  475. void *pDest = NULL;
  476. if ( bBSPFile )
  477. {
  478. pDest = (char *) m_bufContents.Base();
  479. }
  480. else
  481. {
  482. pDest = (char *) m_bufContents.Base() + nOffset;
  483. }
  484. int32 nBytesRead = pRemoteStorage->UGCRead( m_hCloudID, pDest, nChunkSize, nOffset, k_EUGCRead_ContinueReadingUntilFinished );
  485. if ( nBytesRead <= 0 )
  486. {
  487. ThrowError( "[UGC] Failed call to UGCRead on cloud file %s (%llu)\n", pResult->m_pchFileName, m_hCloudID );
  488. return;
  489. }
  490. if ( bBSPFile )
  491. {
  492. g_pFullFileSystem->Write( m_bufContents.Base(), nBytesRead, fh );
  493. if ( nOffset == 0 && IsZip( m_bufContents.Base() ) )
  494. {
  495. bZipFile = true;
  496. }
  497. }
  498. nOffset += nBytesRead;
  499. }
  500. if ( bBSPFile )
  501. {
  502. g_pFullFileSystem->Close( fh );
  503. m_bufContents.Purge();
  504. }
  505. #ifdef LOG_FILEREQUEST_PROGRESS
  506. Log_Msg( LOG_WORKSHOP, "[UGC] Start unzip file %s%c%s (%llu) (%f)\n", m_szTargetDirectory, CORRECT_PATH_SEPARATOR, m_szTargetFilename, m_hCloudID, gpGlobals->realtime );
  507. #endif // LOG_FILEREQUEST_PROGRESS
  508. // If authors rename the map file, old versions get orphaned in the workshop directory. Nuke any bsp here.
  509. if ( cl_remove_old_ugc_downloads.GetBool() )
  510. {
  511. CUtlVector<CUtlString> outList;
  512. AddFilesToList( outList, m_szTargetDirectory, "MOD", "bsp" );
  513. FOR_EACH_VEC( outList, i )
  514. {
  515. filesystem->RemoveFile( outList[i] );
  516. }
  517. }
  518. if ( bZipFile )
  519. {
  520. g_pFullFileSystem->RelativePathToFullPath( szZipFullPath, "MOD", szZipFullPath, MAX_PATH );
  521. m_UGCStatus = UGCFILEREQUEST_UNZIPPING;
  522. m_pUnzipJob = new CUGCUnzipJob( szLocalFullPath, szZipFullPath );
  523. m_pUnzipJob->SetFlags( JF_IO );
  524. g_pUGCUnzipThreadPool->AddJob( m_pUnzipJob );
  525. #ifdef LOG_FILEREQUEST_PROGRESS
  526. Log_Msg( LOG_WORKSHOP, "[UGC] Unzipping started for %s (%llu) (%f)\n", szLocalFullPath, m_hCloudID, gpGlobals->realtime );
  527. #endif // LOG_FILEREQUEST_PROGRESS
  528. }
  529. else if ( bBSPFile )
  530. {
  531. // file was downloaded with .zip extension, but it's not zipped, so rename it to the target name (bsp) and we're done
  532. filesystem->RenameFile( szZipFullPath, szLocalFullPath, "MOD" );
  533. MarkCompleteAndFree();
  534. }
  535. else
  536. {
  537. char szLocalFullPath[MAX_PATH];
  538. V_strcpy_safe( szLocalFullPath, GetDirectory() );
  539. g_pFullFileSystem->RelativePathToFullPath( szLocalFullPath, "MOD", szLocalFullPath, MAX_PATH );
  540. V_SafeComposeFilename( szLocalFullPath, GetFilename(), szLocalFullPath, sizeof(szLocalFullPath) );
  541. // Async write this to disc with monitoring
  542. if ( g_pFullFileSystem->AsyncWrite( szLocalFullPath, m_bufContents.Base(), m_bufContents.TellPut(), false, false, &m_AsyncControl ) < 0 )
  543. {
  544. // Async write failed immediately!
  545. ThrowError( "[UGC] Async write of downloaded file %s failed\n", szLocalFullPath );
  546. return;
  547. }
  548. #ifdef LOG_FILEREQUEST_PROGRESS
  549. Log_Msg( LOG_WORKSHOP, "[UGC] Async write started for %s (%llu) (%f)\n", szLocalFullPath, m_hCloudID, gpGlobals->realtime );
  550. #endif // LOG_FILEREQUEST_PROGRESS
  551. // Mark us as having started out download
  552. m_UGCStatus = UGCFILEREQUEST_DOWNLOAD_WRITING;
  553. }
  554. }
  555. void CUGCFileRequest::UpdateUnzip()
  556. {
  557. if ( m_pUnzipJob->IsFinished() )
  558. {
  559. // clean up zip file
  560. char szZipFullPath[ MAX_PATH ];
  561. GetFullPath( szZipFullPath, sizeof(szZipFullPath) );
  562. V_SetExtension( szZipFullPath, ".zip", MAX_PATH );
  563. filesystem->RemoveFile( szZipFullPath, "MOD" );
  564. MarkCompleteAndFree();
  565. }
  566. }
  567. //-----------------------------------------------------------------------------
  568. // Purpose: Poll for status and drive the process forward
  569. //-----------------------------------------------------------------------------
  570. UGCFileRequestStatus_t CUGCFileRequest::Update( void )
  571. {
  572. switch ( m_UGCStatus )
  573. {
  574. case UGCFILEREQUEST_UNZIPPING:
  575. {
  576. UpdateUnzip();
  577. return m_UGCStatus;
  578. }
  579. break;
  580. // Handle the async write of the file to disc
  581. case UGCFILEREQUEST_DOWNLOAD_WRITING:
  582. {
  583. #ifdef FILEREQUEST_IO_STALL
  584. if ( m_nIOStallType == FILEREQUEST_STALL_WRITE )
  585. {
  586. // If we're stalling, then pretend that we're going at a uniformly slow pace through the duration of the stall time
  587. const float flStallTime = gpGlobals->realtime - m_flIOStallStart;
  588. m_flDownloadProgress = RemapValClamped( flStallTime, 0, m_flIOStallDuration, 0.0f, 1.0f );
  589. if ( flStallTime < m_flIOStallDuration )
  590. return UGCFILEREQUEST_DOWNLOAD_WRITING;
  591. }
  592. #endif // FILEREQUEST_IO_STALL
  593. // Monitor the async write progress and clean up after we're done
  594. if ( m_AsyncControl )
  595. {
  596. FSAsyncStatus_t status = g_pFullFileSystem->AsyncStatus( m_AsyncControl );
  597. switch ( status )
  598. {
  599. case FSASYNC_STATUS_PENDING:
  600. case FSASYNC_STATUS_INPROGRESS:
  601. case FSASYNC_STATUS_UNSERVICED:
  602. return UGCFILEREQUEST_DOWNLOAD_WRITING;
  603. case FSASYNC_ERR_FILEOPEN:
  604. return ThrowError( "[UGC] Unable to write file to disc!\n" );
  605. }
  606. // Finish the read
  607. g_pFullFileSystem->AsyncFinish( m_AsyncControl );
  608. g_pFullFileSystem->AsyncRelease( m_AsyncControl );
  609. m_AsyncControl = NULL;
  610. #ifdef LOG_FILEREQUEST_PROGRESS
  611. Log_Msg( LOG_WORKSHOP, "[UGC] Async write completed for %s%c%s (%llu) (%f)\n", m_szTargetDirectory, CORRECT_PATH_SEPARATOR, m_szTargetFilename, m_hCloudID, gpGlobals->realtime );
  612. #endif // LOG_FILEREQUEST_PROGRESS
  613. MarkCompleteAndFree();
  614. return m_UGCStatus;
  615. }
  616. // Somehow we lost the handle to our async status or got a spurious call in here!
  617. return ThrowError( "[UGC] Lost handle to async handle for downloaded file write!\n" );
  618. }
  619. break;
  620. // Handle starting up a download
  621. case UGCFILEREQUEST_READY:
  622. case UGCFILEREQUEST_UPLOADING:
  623. return m_UGCStatus;
  624. break;
  625. case UGCFILEREQUEST_FINISHED:
  626. // Progress is complete
  627. m_flDownloadProgress = 1.0f;
  628. return m_UGCStatus;
  629. break;
  630. case UGCFILEREQUEST_DOWNLOADING:
  631. {
  632. #ifdef FILEREQUEST_IO_STALL
  633. // If we're stalling, then pretend that we're going at a uniformly slow pace through the duration of the stall time
  634. m_flDownloadProgress = RemapValClamped( ( gpGlobals->realtime - m_flIOStallStart ), 0, m_flIOStallDuration, 0.0f, 1.0f );
  635. #else
  636. // Find the progress of our current download
  637. int32 nBytesDownloaded, nBytesExpected;
  638. GetISteamRemoteStorage()->GetUGCDownloadProgress( m_hCloudID, &nBytesDownloaded, &nBytesExpected );
  639. if ( nBytesExpected != 0 )
  640. {
  641. // Store off our progress on this file
  642. m_flDownloadProgress = ((float)nBytesDownloaded/(float)nBytesExpected);
  643. }
  644. #endif // FILEREQUEST_IO_STALL
  645. return m_UGCStatus;
  646. }
  647. break;
  648. // An error has occurred while trying to handle the user's request
  649. default:
  650. case UGCFILEREQUEST_ERROR:
  651. return UGCFILEREQUEST_ERROR;
  652. break;
  653. }
  654. }
  655. //-----------------------------------------------------------------------------
  656. // Purpose: Get the local file name on disk, accounting for target directories and filenames
  657. //-----------------------------------------------------------------------------
  658. void CUGCFileRequest::GetFullPath( char *pDest, size_t strSize ) const
  659. {
  660. V_SafeComposeFilename( GetDirectory(), GetFilename(), pDest, strSize );
  661. }
  662. //-----------------------------------------------------------------------------
  663. // Purpose: Name on disk if not the same as in the cloud
  664. //-----------------------------------------------------------------------------
  665. const char *CUGCFileRequest::GetFilename( void ) const
  666. {
  667. return ( m_szTargetFilename[0] == '\0' ) ? m_szFileName : m_szTargetFilename;
  668. }
  669. //-----------------------------------------------------------------------------
  670. // Purpose: Get the local directory on disk, accounting for target directories
  671. //-----------------------------------------------------------------------------
  672. const char *CUGCFileRequest::GetDirectory( void ) const
  673. {
  674. return ( m_szTargetDirectory[0] == '\0' ) ? NULL : m_szTargetDirectory;
  675. }
  676. //
  677. // Marks the file request as complete and frees its internal buffers
  678. //
  679. void CUGCFileRequest::MarkCompleteAndFree( bool bUpdated /*= true*/ )
  680. {
  681. m_bufContents.Purge();
  682. m_UGCStatus = UGCFILEREQUEST_FINISHED;
  683. #ifdef LOG_FILEREQUEST_PROGRESS
  684. char szFilename[MAX_PATH];
  685. GetFullPath( szFilename, ARRAYSIZE(szFilename) );
  686. Log_Msg( LOG_WORKSHOP, "[UGC] File %s (%llu) finished all operations! (Duration: %f seconds)\n", szFilename, m_hCloudID, gpGlobals->realtime-m_flIOStartTime );
  687. #endif // LOG_FILEREQUEST_PROGRESS
  688. #ifdef CLIENT_DLL
  689. if ( StringHasPrefix( GetFilename(), "thumb" ) && V_strstr( GetFilename(), ".jpg" ) )
  690. {
  691. CreateSmallThumbNail( bUpdated );
  692. }
  693. #endif
  694. //
  695. // Timestamp the file to match workshop updated timestamp
  696. //
  697. if ( m_tFileUpdateTime )
  698. UGCUtil_TimestampFile( szFilename, m_tFileUpdateTime );
  699. if ( m_pUnzipJob )
  700. {
  701. m_pUnzipJob->Release();
  702. m_pUnzipJob = NULL;
  703. }
  704. }
  705. #ifdef CLIENT_DLL
  706. void CUGCFileRequest::CreateSmallThumbNail( bool bForce )
  707. {
  708. char szFilename[ MAX_PATH ];
  709. GetFullPath( szFilename, sizeof(szFilename) );
  710. char szFullFilename[ MAX_PATH ];
  711. g_pFullFileSystem->RelativePathToFullPath( szFilename, "MOD", szFullFilename, sizeof( szFullFilename ) );
  712. char szSmallFilename[ MAX_PATH ];
  713. V_strncpy( szSmallFilename, szFullFilename, sizeof(szSmallFilename) );
  714. char *pchExt = V_strrchr( szSmallFilename, '.' );
  715. if ( !pchExt )
  716. return;
  717. *pchExt = '\0';
  718. V_strncat( szSmallFilename, "_s.jpg", sizeof( szSmallFilename ) );
  719. if ( !bForce && g_pFullFileSystem->FileExists( szSmallFilename ) )
  720. return;
  721. int width, height;
  722. ConversionErrorType errCode;
  723. unsigned char *pThumbnailData = ImgUtl_ReadImageAsRGBA( szFullFilename, width, height, errCode );
  724. if ( errCode != CE_SUCCESS )
  725. {
  726. DevMsg( "Failed to read thumbnail %s.\n", szFullFilename );
  727. return;
  728. }
  729. // Now convert the image to an appropriate size for preview
  730. unsigned char *pThumbnailSmallData = NULL;
  731. const unsigned int nThumbnailSmallWidth = THUMBNAIL_SMALL_WIDTH;
  732. const unsigned int nThumbnailSmallHeight = THUMBNAIL_SMALL_HEIGHT;
  733. if ( !ResizeRGBAImage( pThumbnailData, (width*height*4), width, height, &pThumbnailSmallData, nThumbnailSmallWidth, nThumbnailSmallHeight ) )
  734. {
  735. DevMsg( "Failed to resize small thumbnail %s.\n", szSmallFilename );
  736. free( pThumbnailData );
  737. return;
  738. }
  739. if ( ImgUtl_WriteRGBAToJPEG( pThumbnailSmallData, nThumbnailSmallWidth, nThumbnailSmallHeight, szSmallFilename ) != CE_SUCCESS )
  740. {
  741. DevMsg( "Failed to write small thumbnail %s.\n", szSmallFilename );
  742. }
  743. free( pThumbnailData );
  744. free( pThumbnailSmallData );
  745. }
  746. //-----------------------------------------------------------------------------
  747. // Purpose: Validate, resize and convert a RGBA to a properly formatted size
  748. //-----------------------------------------------------------------------------
  749. bool CUGCFileRequest::ResizeRGBAImage( const unsigned char *pData, unsigned int nDataSize, unsigned int nWidth, unsigned int nHeight,
  750. unsigned char **pDataOut, unsigned int nNewWidth, unsigned int nNewHeight )
  751. {
  752. // Find out how to squish the image to fit within our necessary borders
  753. float flFrameRatio = ( (float) nNewWidth / (float) nNewWidth );
  754. float flSourceRatio = ( (float) nWidth / (float) nHeight );
  755. unsigned int nScaleWidth;
  756. unsigned int nScaleHeight;
  757. if ( flSourceRatio < flFrameRatio )
  758. {
  759. nScaleWidth = nNewWidth;
  760. nScaleHeight = ( nNewWidth / flSourceRatio );
  761. }
  762. else if ( flSourceRatio > flFrameRatio )
  763. {
  764. nScaleWidth = ( nNewHeight * flSourceRatio );
  765. nScaleHeight = nNewHeight;
  766. }
  767. else
  768. {
  769. nScaleWidth = nNewWidth;
  770. nScaleHeight = nNewHeight;
  771. }
  772. // Allocate a buffer to hold the scaled image
  773. unsigned char *pScaleBuf = (unsigned char *) malloc( nScaleWidth * nScaleHeight * 4 );
  774. if ( pScaleBuf == NULL )
  775. return false;
  776. // FIXME: Combine these helper functions into one operation, rather than multiple!
  777. // Scale the image to the proper size
  778. if ( ImgUtl_StretchRGBAImage( pData, nWidth, nHeight, pScaleBuf, nScaleWidth, nScaleHeight ) == CE_SUCCESS )
  779. {
  780. // Allocate a buffer to pad this image out to
  781. *pDataOut = (unsigned char *) malloc( nNewWidth * nNewHeight * 4 );
  782. if ( *pDataOut == NULL )
  783. {
  784. free( pScaleBuf );
  785. return false;
  786. }
  787. // Calc the offset for the image to be centered after cropping
  788. unsigned int cropX = ( nScaleWidth - nNewWidth ) / 2;
  789. unsigned int cropY = ( nScaleHeight - nNewHeight ) / 2;
  790. // Crop the image down to size
  791. if ( ImgUtl_CropRGBA( cropX, cropY, nScaleWidth, nScaleHeight, nNewWidth, nNewHeight, pScaleBuf, *pDataOut ) != CE_SUCCESS )
  792. {
  793. free( *pDataOut );
  794. return false;
  795. }
  796. }
  797. // Release it!
  798. free( pScaleBuf );
  799. return true;
  800. }
  801. #endif
  802. //
  803. // Sets the file request into an error state
  804. //
  805. UGCFileRequestStatus_t CUGCFileRequest::ThrowError( const char *lpszFormat, ... )
  806. {
  807. va_list marker;
  808. va_start( marker, lpszFormat );
  809. Q_vsnprintf( m_szErrorText, sizeof( m_szErrorText ), lpszFormat, marker );
  810. va_end( marker );
  811. #ifdef LOG_FILEREQUEST_PROGRESS
  812. Log_Warning( LOG_WORKSHOP, "%s", m_szErrorText );
  813. #endif // LOG_FILEREQUEST_PROGRESS
  814. m_UGCStatus = UGCFILEREQUEST_ERROR;
  815. return m_UGCStatus;
  816. }
  817. #endif // !NO_STEAM
  818. //-----------------------------------------------------------------------------
  819. // Purpose: Same as V_ComposeFilename but can deal with NULL pointers for the directory (meaning non-existant)
  820. //-----------------------------------------------------------------------------
  821. void V_SafeComposeFilename( const char *pPathIn, const char *pFilenameIn, char *pDest, size_t nDestSize )
  822. {
  823. // If we've passed in a directory, then start with it
  824. if ( pPathIn != NULL )
  825. {
  826. V_strncpy( pDest, pPathIn, nDestSize );
  827. V_FixSlashes( pDest );
  828. V_AppendSlash( pDest, nDestSize );
  829. }
  830. else
  831. {
  832. // Make sure we're clear
  833. pDest[0] = '\0';
  834. }
  835. if ( pFilenameIn != NULL )
  836. {
  837. // Tack on the filename and fix slashes
  838. V_strncat( pDest, pFilenameIn, nDestSize, COPY_ALL_CHARACTERS );
  839. V_FixSlashes( pDest );
  840. }
  841. }
  842. bool UnzipFile( const char* szPathToZipFile, const char* szOutputDir /*= NULL*/)
  843. {
  844. char outPath[MAX_PATH];
  845. if ( szOutputDir )
  846. {
  847. V_strcpy_safe( outPath, szOutputDir );
  848. }
  849. else
  850. {
  851. V_ExtractFilePath( szPathToZipFile, outPath, sizeof(outPath) );
  852. }
  853. bool bSuccess = false;
  854. HZIP hz = OpenZip( (void*)szPathToZipFile, 0, ZIP_FILENAME );
  855. if ( hz )
  856. {
  857. ZIPENTRY ze;
  858. ZRESULT zr = GetZipItem( hz, -1, &ze );
  859. if ( zr == ZR_OK )
  860. {
  861. uint32 count = ze.index;
  862. uint32 successCount = 0;
  863. for ( uint32 i = 0; i < count; ++i )
  864. {
  865. if ( ZR_OK == GetZipItem( hz, i, &ze ) )
  866. {
  867. char dest[MAX_PATH];
  868. V_ComposeFileName( outPath, ze.name, dest, sizeof(dest) );
  869. zr = UnzipItem( hz, i, (void*)dest, 0, ZIP_FILENAME );
  870. if ( zr == ZR_OK )
  871. {
  872. successCount++;
  873. }
  874. else
  875. {
  876. char errorBuf[256];
  877. FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) );
  878. Warning( "Failed unzipping entry '%s' in zip file '%s'. Reason: %s \n", ze.name, szPathToZipFile, errorBuf );
  879. }
  880. }
  881. }
  882. bSuccess = count == successCount;
  883. }
  884. else
  885. {
  886. char errorBuf[256];
  887. FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) );
  888. Warning( "Failed to get count of items in zip file '%s'. Reason: %s \n", szPathToZipFile, errorBuf );
  889. }
  890. CloseZip( hz );
  891. }
  892. else
  893. {
  894. char errorBuf[256];
  895. FormatZipMessage( ZR_RECENT, errorBuf, sizeof(errorBuf ) );
  896. Warning( "Failed to open zip file '%s'. Reason: %s\n", szPathToZipFile, errorBuf );
  897. }
  898. return bSuccess;
  899. }
  900. bool UGCUtil_IsOfficialMap( PublishedFileId_t id )
  901. {
  902. /** Removed for partner depot **/
  903. return false;
  904. }
  905. //-----------------------------------------------------------------------------
  906. // Purpose: Get the local file name on disk, accounting for target directories and filenames
  907. //-----------------------------------------------------------------------------
  908. void CUGCFileRequest::GetLocalFileName( char *pDest, size_t strSize )
  909. {
  910. if ( m_szTargetDirectory[0] == '\0' )
  911. {
  912. V_strncpy( pDest, GetFilename(), strSize );
  913. }
  914. else
  915. {
  916. V_snprintf( pDest, strSize, "%s/%s", m_szTargetDirectory, GetFilename() );
  917. }
  918. }
  919. //-----------------------------------------------------------------------------
  920. // Purpose: Get the local directory on disk, accounting for target directories
  921. //-----------------------------------------------------------------------------
  922. void CUGCFileRequest::GetLocalDirectory( char *pDest, size_t strSize )
  923. {
  924. if ( m_szTargetDirectory[0] == '\0' )
  925. {
  926. V_strncpy( pDest, "\0", strSize );
  927. }
  928. else
  929. {
  930. V_strncpy( pDest, m_szTargetDirectory, strSize );
  931. }
  932. }