|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Utility helper functions for dealing with UGC files
//
//==========================================================================//
#include "cbase.h"
#include "ugc_utils.h"
#include "logging.h"
#include "globalvars_base.h"
#include "strtools.h"
#ifdef CLIENT_DLL
#include "imageutils.h"
#endif
#include "zip/XUnzip.h"
#include "vstdlib/jobthread.h"
#include "tier2/fileutils.h"
#if defined( _WIN32 )
#include <sys/utime.h>
#elif defined(OSX)
#include <utime.h>
#else
#include <sys/types.h>
#include <utime.h>
#endif
// FIXME: These need to be properly arranged to make this viable!
extern IFileSystem *filesystem;
#define FILEREQUEST_IO_STALL_DELAY 30.0f // Seconds
#define DOWNLOAD_CHUNK_SIZE 10485760 // 10MB
#define THUMBNAIL_SMALL_WIDTH 256
#define THUMBNAIL_SMALL_HEIGHT 144
#if !defined( NO_STEAM )
extern CSteamAPIContext *steamapicontext; // available on game clients
#endif // !NO_STEAM
#if !defined( NO_STEAM ) && !defined ( _PS3 )
Color g_WorkshopLogColor( 0, 255, 255, 255 ); BEGIN_DEFINE_LOGGING_CHANNEL( LOG_WORKSHOP, "Workshop", LCF_CONSOLE_ONLY, LS_WARNING, g_WorkshopLogColor ); ADD_LOGGING_CHANNEL_TAG( "UGCOperation" ); ADD_LOGGING_CHANNEL_TAG( "WorkshopOperation" ); END_DEFINE_LOGGING_CHANNEL();
ConVar cl_remove_old_ugc_downloads( "cl_remove_old_ugc_downloads", "1", FCVAR_RELEASE );
//-----------------------------------------------------------------------------
// Purpose: Helper function for Steam's remote storage interface
//-----------------------------------------------------------------------------
ISteamRemoteStorage *GetISteamRemoteStorage() { return ( steamapicontext != NULL ) ? steamapicontext->SteamRemoteStorage() : NULL; }
//-----------------------------------------------------------------------------
// Purpose: Helper function to get the map Published File ID from a map path
//-----------------------------------------------------------------------------
PublishedFileId_t GetMapIDFromMapPath( const char *pMapPath ) { char tmp[MAX_PATH]; V_strcpy_safe( tmp, pMapPath ); V_FixSlashes( tmp, '/' ); // internal path strings use forward slashes, make sure we compare like that.
if ( V_strstr( tmp, "workshop/" ) ) { V_StripFilename(tmp); V_StripTrailingSlash(tmp); const char* szDirName = V_GetFileName(tmp); return (PublishedFileId_t)V_atoui64(szDirName); }
return 0; }
bool UGCUtil_TimestampFile( const char *pFileRelativePath, uint32 uTimestamp ) { char chFullFilePathForTimestamp[ MAX_PATH ] = {0}; if ( char const *pchFullPath = g_pFullFileSystem->RelativePathToFullPath( pFileRelativePath, "MOD", chFullFilePathForTimestamp, sizeof( chFullFilePathForTimestamp ) ) ) { struct utimbuf tbuffer; tbuffer.modtime = tbuffer.actime = uTimestamp; int iResultCode = utime( pchFullPath, &tbuffer );
// There is an inconsistency between what utime writes and what stat returns due to daylight savings.
// 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.
uint32 unFileTimeFromStat = (uint32)g_pFullFileSystem->GetFileTime( pFileRelativePath, "MOD" ); if ( unFileTimeFromStat != uTimestamp ) { int32 nDLSOffset = unFileTimeFromStat - uTimestamp; tbuffer.modtime = tbuffer.actime = uTimestamp - nDLSOffset; iResultCode = utime( pchFullPath, &tbuffer ); #if defined ( DEBUG )
unFileTimeFromStat = (uint32)g_pFullFileSystem->GetFileTime( pFileRelativePath, "MOD" ); Assert( unFileTimeFromStat == uTimestamp ); #endif
} return ( iResultCode == 0 ); } return false; }
inline bool IsZip( void *z ) { return ( z && *(unsigned int *)z == 0x04034b50 ); }
//-----------------------------------------------------------------------------
// CUGCUnzipper
//-----------------------------------------------------------------------------
class CUGCUnzipJob : public CJob { public: CUGCUnzipJob( const char *szTargetFile, const char *szTempFile ); virtual JobStatus_t DoExecute(); bool IsFinished ( void ) const { return m_bIsFinished; }
private: bool m_bIsFinished; char m_szTargetFile[MAX_PATH]; // Game dir relative path for unzipped file
char m_szTempFile[MAX_PATH];
};
CUGCUnzipJob::CUGCUnzipJob( const char *szTargetFile, const char *szTempFile ) { V_strcpy_safe( m_szTargetFile, szTargetFile ); V_strcpy_safe( m_szTempFile, szTempFile ); m_bIsFinished = false; }
JobStatus_t CUGCUnzipJob::DoExecute() { CUtlBuffer unzipBuf; const uint32 unUnzipBufSize = 10 * 1024 * 1024; unzipBuf.EnsureCapacity( unUnzipBufSize );
if ( HZIP hz = OpenZip( m_szTempFile, 0, ZIP_FILENAME ) ) { ZIPENTRY ze; ZRESULT zr = GetZipItem( hz, -1, &ze ); // get count
if ( zr == ZR_OK ) { Assert( ze.index == 1 ); // This code assumes there is just a single file in the zip.
if ( ZR_OK == GetZipItem( hz, 0, &ze ) ) { FileHandle_t fh = g_pFullFileSystem->Open( m_szTargetFile, "wb", "MOD" ); uint32 unBytesWritten = 0; do { zr = UnzipItem( hz, 0, unzipBuf.Base(), unUnzipBufSize, ZIP_MEMORY ); uint32 unBytesToWrite = MIN( unUnzipBufSize, ze.unc_size - unBytesWritten ); if ( unBytesToWrite > 0 ) unBytesWritten += g_pFullFileSystem->Write( unzipBuf.Base(), unBytesToWrite, fh ); } while ( zr == ZR_MORE );
if ( zr != ZR_OK ) { char errorBuf[256]; FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) ); Warning( "Failed unzipping entry '%s'. Reason: %s \n", ze.name, errorBuf ); } g_pFullFileSystem->Close( fh ); } } else { char errorBuf[256]; FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) ); Warning( "Failed to get count of items. Reason: %s \n", errorBuf ); } CloseZip( hz ); } m_bIsFinished = true; return JOB_OK; }
IThreadPool *g_pUGCUnzipThreadPool = NULL;
void UGCUtil_Shutdown() { if ( g_pUGCUnzipThreadPool ) { g_pUGCUnzipThreadPool->Stop(); DestroyThreadPool( g_pUGCUnzipThreadPool ); g_pUGCUnzipThreadPool = NULL; } }
void UGCUtil_Init() { Assert( g_pUGCUnzipThreadPool == NULL ); if ( !g_pUGCUnzipThreadPool ) { ThreadPoolStartParams_t params; params.nThreads = 1; params.nStackSize = 1024*1024; params.fDistribute = TRS_FALSE; g_pUGCUnzipThreadPool= CreateNewThreadPool(); g_pUGCUnzipThreadPool->Start( params, "UGCUnzipThreadPool" ); } }
//-----------------------------------------------------------------------------
// Constructor
//-----------------------------------------------------------------------------
CUGCFileRequest::CUGCFileRequest( void ) : m_hCloudID( k_UGCHandleInvalid ), m_UGCStatus( UGCFILEREQUEST_READY ), m_AsyncControl( NULL ), m_flIOStartTime( 0 ), m_flDownloadProgress( 0.0f ), m_tFileUpdateTime( 0 ), m_pUnzipJob( NULL ) { // Start with these disabled
m_szFileName[0] = '\0'; m_szTargetDirectory[0] = '\0'; m_szTargetFilename[0] = '\0'; m_szErrorText[0] = '\0';
#ifdef FILEREQUEST_IO_STALL
m_nIOStallType = FILEREQUEST_STALL_DOWNLOAD;//FILEREQUEST_STALL_WRITE;
m_flIOStallDuration = FILEREQUEST_IO_STALL_DELAY; // seconds
#endif // FILEREQUEST_IO_STALL
}
//-----------------------------------------------------------------------------
// Destructor
//-----------------------------------------------------------------------------
CUGCFileRequest::~CUGCFileRequest( void ) { // Finish the file i/o
if ( m_AsyncControl != NULL ) { g_pFullFileSystem->AsyncFinish( m_AsyncControl ); g_pFullFileSystem->AsyncRelease( m_AsyncControl ); m_AsyncControl = NULL; }
// Clear our internal buffer
m_bufContents.Purge(); }
//-----------------------------------------------------------------------------
// Purpose: Check if the file is in sync with the cloud
//-----------------------------------------------------------------------------
bool CUGCFileRequest::FileInSync( const char *lpszTargetDirectory, const char *lpszTargetFilename, uint32 timeUpdated ) { if ( lpszTargetFilename == NULL ) return false;
char chCheckTargetDirectory[MAX_PATH] = {0}; V_strncpy( chCheckTargetDirectory, lpszTargetDirectory, sizeof( chCheckTargetDirectory ) ); V_FixSlashes( chCheckTargetDirectory, '/' ); if ( const char *pszWorkshopMapId = StringAfterPrefix( chCheckTargetDirectory, "maps/workshop/" ) ) { PublishedFileId_t uiWorkshopMapId = Q_atoui64( pszWorkshopMapId ); if ( UGCUtil_IsOfficialMap( uiWorkshopMapId ) ) return true; }
#ifdef FILEREQUEST_IO_STALL
return false; #endif // FILEREQUEST_IO_STALL
char szFilename[MAX_PATH]; V_SafeComposeFilename( lpszTargetDirectory, lpszTargetFilename, szFilename, ARRAYSIZE(szFilename) );
// If the file exists, we need to check it's information
if ( g_pFullFileSystem->FileExists( szFilename ) ) { if ( timeUpdated != 0 ) { // mtime needs to match the time last updated exactly, as we slam the file time when we download
// so an earlier time is out of date and a later time may be modified due to file copying.
uint32 fileTime = (uint32) g_pFullFileSystem->GetFileTime( szFilename, "MOD" ); if ( timeUpdated == fileTime ) return true; } else { // We didn't supply a time to check against, so we only cared about its existence
return true; } }
return false; }
//-----------------------------------------------------------------------------
// Purpose: Start a download by handle
//-----------------------------------------------------------------------------
UGCFileRequestStatus_t CUGCFileRequest::StartDownload( UGCHandle_t hFileHandle, const char *lpszTargetDirectory /*= NULL*/, const char *lpszTargetFilename /*= NULL*/, uint32 timeUpdated /*=0*/, bool bForceUpdate /*=false*/ ) { // Start with the assumption of failure
m_UGCStatus = UGCFILEREQUEST_ERROR; m_tFileUpdateTime = timeUpdated;
// First, see if this file is already down on the disk (unless we're overriding the call)
if ( bForceUpdate == false && FileInSync( lpszTargetDirectory, lpszTargetFilename, timeUpdated ) ) { m_hCloudID = hFileHandle;
// Take a target directory for the file
if ( lpszTargetDirectory != NULL ) { V_strncpy( m_szTargetDirectory, lpszTargetDirectory, MAX_PATH ); V_FixSlashes( m_szTargetDirectory ); }
// Take a target filename for the file
if ( lpszTargetFilename != NULL ) { V_strncpy( m_szTargetFilename, lpszTargetFilename, MAX_PATH ); }
#ifdef LOG_FILEREQUEST_PROGRESS
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 ); #endif // LOG_FILEREQUEST_PROGRESS
MarkCompleteAndFree( false ); return m_UGCStatus; }
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] Beginning download of %s%c%s. (%f)\n", lpszTargetDirectory, CORRECT_PATH_SEPARATOR, lpszTargetFilename, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
// Start the download request
uint32 nPriority = 0; // FIXME: For now, we always download at an equal priority
SteamAPICall_t hSteamAPICall = GetISteamRemoteStorage()->UGCDownload( hFileHandle, nPriority ); m_callbackUGCDownload.Set( hSteamAPICall, this, &CUGCFileRequest::Steam_OnUGCDownload );
if ( hSteamAPICall != k_uAPICallInvalid ) { // Mark download as in progress
m_UGCStatus = UGCFILEREQUEST_DOWNLOADING; m_hCloudID = hFileHandle; m_flIOStartTime = gpGlobals->realtime;
// Take a target directory for the file
if ( lpszTargetDirectory != NULL ) { V_strncpy( m_szTargetDirectory, lpszTargetDirectory, MAX_PATH ); V_FixSlashes( m_szTargetDirectory ); }
// Take a target filename for the file
if ( lpszTargetFilename != NULL ) { V_strncpy( m_szTargetFilename, lpszTargetFilename, MAX_PATH ); }
#ifdef FILEREQUEST_IO_STALL
m_flIOStallStart = gpGlobals->realtime; #endif // FILEREQUEST_IO_STALL
// Start with an initialized value for our progress
m_flDownloadProgress = 0.0f;
// Done!
return m_UGCStatus; }
// We were unable to start our download through the Steam API
return ThrowError( "[UGC] Failed to initiate download of file (%s%s%s) from cloud\n", lpszTargetDirectory, CORRECT_PATH_SEPARATOR, lpszTargetFilename ); }
//-----------------------------------------------------------------------------
// Purpose: Start an upload of a buffer by filename
//-----------------------------------------------------------------------------
UGCFileRequestStatus_t CUGCFileRequest::StartUpload( CUtlBuffer &buffer, const char *lpszFilename ) { // Start with the assumption of failure
m_UGCStatus = UGCFILEREQUEST_ERROR;
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] Saving %s to user cloud. (%f)\n", lpszFilename, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
// Write the local copy of the file
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage(); if ( pRemoteStorage == NULL ) return ThrowError( "[UGC] Failed to write file to cloud\n" );
UGCFileWriteStreamHandle_t hWriteStream = pRemoteStorage->FileWriteStreamOpen( lpszFilename ); if ( hWriteStream == k_UGCFileStreamHandleInvalid ) return ThrowError( "[UGC] Failed to write file to cloud\n" );
uint32 nBytesWritten = 0; uint32 nBytesToWrite = buffer.TellPut(); while ( nBytesToWrite ) { uint32 nChunkSize = MIN( nBytesToWrite, (100*1024*1024) ); // 100Mb limit
if ( !pRemoteStorage->FileWriteStreamWriteChunk( hWriteStream, buffer.PeekGet(nBytesWritten), nChunkSize ) ) { // NOTE: This won't be necessary in future updates
pRemoteStorage->FileWriteStreamCancel( hWriteStream ); return ThrowError( "[UGC] Failed to write file to cloud\n" ); }
// Decrement the amount of bytes remaining
nBytesToWrite -= nChunkSize; }
if ( !pRemoteStorage->FileWriteStreamClose( hWriteStream ) ) { return ThrowError( "[UGC] Failed to write file to cloud\n" ); }
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] Sharing %s to user cloud. (%f)\n", lpszFilename, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
// Now share the file (uploads it to the cloud)
SteamAPICall_t hSteamAPICall = pRemoteStorage->FileShare( lpszFilename ); m_callbackFileShare.Set( hSteamAPICall, this, &CUGCFileRequest::Steam_OnFileShare );
#ifdef FILEREQUEST_IO_STALL
m_flIOStallStart = gpGlobals->realtime; #endif // FILEREQUEST_IO_STALL
// Now, hold onto the filename
V_ExtractFilePath( lpszFilename, m_szTargetDirectory, ARRAYSIZE( m_szTargetDirectory ) ); V_StripTrailingSlash( m_szTargetDirectory ); V_FixSlashes( m_szTargetDirectory ); V_strncpy( m_szTargetFilename, V_UnqualifiedFileName( lpszFilename ), ARRAYSIZE( m_szTargetFilename ) );
m_UGCStatus = UGCFILEREQUEST_UPLOADING; m_flIOStartTime = gpGlobals->realtime;
return m_UGCStatus; }
//-----------------------------------------------------------------------------
// Purpose: FileShare complete for a file request
//-----------------------------------------------------------------------------
void CUGCFileRequest::Steam_OnFileShare( RemoteStorageFileShareResult_t *pResult, bool bError ) { char szFilename[MAX_PATH]; GetFullPath( szFilename, ARRAYSIZE(szFilename) ); if ( bError ) { ThrowError( "[UGC] Upload of file %s to Steam cloud failed!\n", szFilename ); return; }
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] File %s shared to user cloud. UGC ID: %llu (%f)\n", szFilename, pResult->m_hFile, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
// Save the return handle
m_hCloudID = pResult->m_hFile;
MarkCompleteAndFree(); }
//-----------------------------------------------------------------------------
// Purpose: UGDownload complete for a file request
//-----------------------------------------------------------------------------
void CUGCFileRequest::Steam_OnUGCDownload( RemoteStorageDownloadUGCResult_t *pResult, bool bError ) { // Completed. Did we succeed?
if ( bError || pResult->m_eResult != k_EResultOK ) { ThrowError( "[UGC] Download of file %s from cloud failed with result %d (%llu)\n", pResult->m_pchFileName, pResult->m_eResult, m_hCloudID ); return; }
// Make sure we got back the file we were expecting
Assert( pResult->m_hFile == m_hCloudID );
// Fetch file details
AppId_t nAppID; char *pchName; int32 nFileSizeInBytes = -1; CSteamID steamIDOwner; ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage(); if ( !pRemoteStorage->GetUGCDetails( m_hCloudID, &nAppID, &pchName, &nFileSizeInBytes, &steamIDOwner ) || nFileSizeInBytes <= 0 ) { ThrowError( "[UGC] Unable to retrieve cloud file %s (%llu) info from Steam\n", pResult->m_pchFileName, pResult->m_hFile ); return; }
// Save our name
V_strncpy( m_szFileName, pchName, sizeof(m_szFileName) );
bool bBSPFile = false; if ( V_strnicmp( V_GetFileExtensionSafe(m_szFileName), "bsp", 3 ) == 0 ) { bBSPFile = true; }
// Take this as our target if we haven't specified one
if ( m_szTargetFilename[0] == '\0' ) { V_strncpy( m_szTargetFilename, V_GetFileName( pchName ), sizeof(m_szTargetFilename) ); }
#ifdef LOG_FILEREQUEST_PROGRESS
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 ); #endif // LOG_FILEREQUEST_PROGRESS
char szLocalFullPath[MAX_PATH]; GetFullPath( szLocalFullPath, sizeof(szLocalFullPath) );
// Make sure the directory exists if we're creating one
if ( m_szTargetDirectory != NULL ) { filesystem->CreateDirHierarchy( m_szTargetDirectory, "MOD" ); } else { char szDirectory[MAX_PATH]; Q_FileBase( szLocalFullPath, szDirectory, sizeof(szDirectory) ); filesystem->CreateDirHierarchy( szDirectory, "MOD" ); }
// Allocate a temporary buffer
m_bufContents.Purge(); if ( bBSPFile ) { m_bufContents.EnsureCapacity( DOWNLOAD_CHUNK_SIZE ); m_bufContents.SeekPut( CUtlBuffer::SEEK_HEAD, DOWNLOAD_CHUNK_SIZE ); } else { m_bufContents.EnsureCapacity( nFileSizeInBytes ); m_bufContents.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSizeInBytes ); }
// Read in the data and save to tmp file
FileHandle_t fh = NULL; char szZipFullPath[ MAX_PATH ]; if ( bBSPFile ) { GetFullPath( szZipFullPath, sizeof(szZipFullPath) ); V_SetExtension( szZipFullPath, ".zip", MAX_PATH ); fh = g_pFullFileSystem->Open( szZipFullPath, "wb", "MOD" ); }
bool bZipFile = false; uint32 nOffset = 0; while ( (int32) nOffset < nFileSizeInBytes ) { uint32 nChunkSize = MIN( (nFileSizeInBytes-nOffset), DOWNLOAD_CHUNK_SIZE ); // 10Mb
void *pDest = NULL; if ( bBSPFile ) { pDest = (char *) m_bufContents.Base(); } else { pDest = (char *) m_bufContents.Base() + nOffset; }
int32 nBytesRead = pRemoteStorage->UGCRead( m_hCloudID, pDest, nChunkSize, nOffset, k_EUGCRead_ContinueReadingUntilFinished ); if ( nBytesRead <= 0 ) { ThrowError( "[UGC] Failed call to UGCRead on cloud file %s (%llu)\n", pResult->m_pchFileName, m_hCloudID ); return; }
if ( bBSPFile ) { g_pFullFileSystem->Write( m_bufContents.Base(), nBytesRead, fh ); if ( nOffset == 0 && IsZip( m_bufContents.Base() ) ) { bZipFile = true; } }
nOffset += nBytesRead; }
if ( bBSPFile ) { g_pFullFileSystem->Close( fh ); m_bufContents.Purge(); }
#ifdef LOG_FILEREQUEST_PROGRESS
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 ); #endif // LOG_FILEREQUEST_PROGRESS
// If authors rename the map file, old versions get orphaned in the workshop directory. Nuke any bsp here.
if ( cl_remove_old_ugc_downloads.GetBool() ) { CUtlVector<CUtlString> outList; AddFilesToList( outList, m_szTargetDirectory, "MOD", "bsp" ); FOR_EACH_VEC( outList, i ) { filesystem->RemoveFile( outList[i] ); } }
if ( bZipFile ) { g_pFullFileSystem->RelativePathToFullPath( szZipFullPath, "MOD", szZipFullPath, MAX_PATH ); m_UGCStatus = UGCFILEREQUEST_UNZIPPING; m_pUnzipJob = new CUGCUnzipJob( szLocalFullPath, szZipFullPath ); m_pUnzipJob->SetFlags( JF_IO ); g_pUGCUnzipThreadPool->AddJob( m_pUnzipJob );
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] Unzipping started for %s (%llu) (%f)\n", szLocalFullPath, m_hCloudID, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
} else if ( bBSPFile ) { // file was downloaded with .zip extension, but it's not zipped, so rename it to the target name (bsp) and we're done
filesystem->RenameFile( szZipFullPath, szLocalFullPath, "MOD" ); MarkCompleteAndFree(); } else { char szLocalFullPath[MAX_PATH]; V_strcpy_safe( szLocalFullPath, GetDirectory() ); g_pFullFileSystem->RelativePathToFullPath( szLocalFullPath, "MOD", szLocalFullPath, MAX_PATH ); V_SafeComposeFilename( szLocalFullPath, GetFilename(), szLocalFullPath, sizeof(szLocalFullPath) );
// Async write this to disc with monitoring
if ( g_pFullFileSystem->AsyncWrite( szLocalFullPath, m_bufContents.Base(), m_bufContents.TellPut(), false, false, &m_AsyncControl ) < 0 ) { // Async write failed immediately!
ThrowError( "[UGC] Async write of downloaded file %s failed\n", szLocalFullPath ); return; }
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Msg( LOG_WORKSHOP, "[UGC] Async write started for %s (%llu) (%f)\n", szLocalFullPath, m_hCloudID, gpGlobals->realtime ); #endif // LOG_FILEREQUEST_PROGRESS
// Mark us as having started out download
m_UGCStatus = UGCFILEREQUEST_DOWNLOAD_WRITING; } }
void CUGCFileRequest::UpdateUnzip() { if ( m_pUnzipJob->IsFinished() ) { // clean up zip file
char szZipFullPath[ MAX_PATH ]; GetFullPath( szZipFullPath, sizeof(szZipFullPath) ); V_SetExtension( szZipFullPath, ".zip", MAX_PATH ); filesystem->RemoveFile( szZipFullPath, "MOD" );
MarkCompleteAndFree(); } }
//-----------------------------------------------------------------------------
// Purpose: Poll for status and drive the process forward
//-----------------------------------------------------------------------------
UGCFileRequestStatus_t CUGCFileRequest::Update( void ) { switch ( m_UGCStatus ) { case UGCFILEREQUEST_UNZIPPING: { UpdateUnzip(); return m_UGCStatus; } break;
// Handle the async write of the file to disc
case UGCFILEREQUEST_DOWNLOAD_WRITING: { #ifdef FILEREQUEST_IO_STALL
if ( m_nIOStallType == FILEREQUEST_STALL_WRITE ) { // If we're stalling, then pretend that we're going at a uniformly slow pace through the duration of the stall time
const float flStallTime = gpGlobals->realtime - m_flIOStallStart; m_flDownloadProgress = RemapValClamped( flStallTime, 0, m_flIOStallDuration, 0.0f, 1.0f ); if ( flStallTime < m_flIOStallDuration ) return UGCFILEREQUEST_DOWNLOAD_WRITING; } #endif // FILEREQUEST_IO_STALL
// Monitor the async write progress and clean up after we're done
if ( m_AsyncControl ) { FSAsyncStatus_t status = g_pFullFileSystem->AsyncStatus( m_AsyncControl ); switch ( status ) { case FSASYNC_STATUS_PENDING: case FSASYNC_STATUS_INPROGRESS: case FSASYNC_STATUS_UNSERVICED: return UGCFILEREQUEST_DOWNLOAD_WRITING;
case FSASYNC_ERR_FILEOPEN: return ThrowError( "[UGC] Unable to write file to disc!\n" ); }
// Finish the read
g_pFullFileSystem->AsyncFinish( m_AsyncControl ); g_pFullFileSystem->AsyncRelease( m_AsyncControl ); m_AsyncControl = NULL;
#ifdef LOG_FILEREQUEST_PROGRESS
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 ); #endif // LOG_FILEREQUEST_PROGRESS
MarkCompleteAndFree(); return m_UGCStatus; }
// Somehow we lost the handle to our async status or got a spurious call in here!
return ThrowError( "[UGC] Lost handle to async handle for downloaded file write!\n" ); } break;
// Handle starting up a download
case UGCFILEREQUEST_READY: case UGCFILEREQUEST_UPLOADING: return m_UGCStatus; break;
case UGCFILEREQUEST_FINISHED: // Progress is complete
m_flDownloadProgress = 1.0f; return m_UGCStatus; break;
case UGCFILEREQUEST_DOWNLOADING: { #ifdef FILEREQUEST_IO_STALL
// If we're stalling, then pretend that we're going at a uniformly slow pace through the duration of the stall time
m_flDownloadProgress = RemapValClamped( ( gpGlobals->realtime - m_flIOStallStart ), 0, m_flIOStallDuration, 0.0f, 1.0f ); #else
// Find the progress of our current download
int32 nBytesDownloaded, nBytesExpected; GetISteamRemoteStorage()->GetUGCDownloadProgress( m_hCloudID, &nBytesDownloaded, &nBytesExpected ); if ( nBytesExpected != 0 ) { // Store off our progress on this file
m_flDownloadProgress = ((float)nBytesDownloaded/(float)nBytesExpected); } #endif // FILEREQUEST_IO_STALL
return m_UGCStatus; } break;
// An error has occurred while trying to handle the user's request
default: case UGCFILEREQUEST_ERROR: return UGCFILEREQUEST_ERROR; break; } }
//-----------------------------------------------------------------------------
// Purpose: Get the local file name on disk, accounting for target directories and filenames
//-----------------------------------------------------------------------------
void CUGCFileRequest::GetFullPath( char *pDest, size_t strSize ) const { V_SafeComposeFilename( GetDirectory(), GetFilename(), pDest, strSize ); }
//-----------------------------------------------------------------------------
// Purpose: Name on disk if not the same as in the cloud
//-----------------------------------------------------------------------------
const char *CUGCFileRequest::GetFilename( void ) const { return ( m_szTargetFilename[0] == '\0' ) ? m_szFileName : m_szTargetFilename; }
//-----------------------------------------------------------------------------
// Purpose: Get the local directory on disk, accounting for target directories
//-----------------------------------------------------------------------------
const char *CUGCFileRequest::GetDirectory( void ) const { return ( m_szTargetDirectory[0] == '\0' ) ? NULL : m_szTargetDirectory; }
//
// Marks the file request as complete and frees its internal buffers
//
void CUGCFileRequest::MarkCompleteAndFree( bool bUpdated /*= true*/ ) { m_bufContents.Purge(); m_UGCStatus = UGCFILEREQUEST_FINISHED;
#ifdef LOG_FILEREQUEST_PROGRESS
char szFilename[MAX_PATH]; GetFullPath( szFilename, ARRAYSIZE(szFilename) );
Log_Msg( LOG_WORKSHOP, "[UGC] File %s (%llu) finished all operations! (Duration: %f seconds)\n", szFilename, m_hCloudID, gpGlobals->realtime-m_flIOStartTime ); #endif // LOG_FILEREQUEST_PROGRESS
#ifdef CLIENT_DLL
if ( StringHasPrefix( GetFilename(), "thumb" ) && V_strstr( GetFilename(), ".jpg" ) ) { CreateSmallThumbNail( bUpdated ); } #endif
//
// Timestamp the file to match workshop updated timestamp
//
if ( m_tFileUpdateTime ) UGCUtil_TimestampFile( szFilename, m_tFileUpdateTime );
if ( m_pUnzipJob ) { m_pUnzipJob->Release(); m_pUnzipJob = NULL; } }
#ifdef CLIENT_DLL
void CUGCFileRequest::CreateSmallThumbNail( bool bForce ) { char szFilename[ MAX_PATH ]; GetFullPath( szFilename, sizeof(szFilename) );
char szFullFilename[ MAX_PATH ]; g_pFullFileSystem->RelativePathToFullPath( szFilename, "MOD", szFullFilename, sizeof( szFullFilename ) );
char szSmallFilename[ MAX_PATH ]; V_strncpy( szSmallFilename, szFullFilename, sizeof(szSmallFilename) );
char *pchExt = V_strrchr( szSmallFilename, '.' ); if ( !pchExt ) return;
*pchExt = '\0'; V_strncat( szSmallFilename, "_s.jpg", sizeof( szSmallFilename ) );
if ( !bForce && g_pFullFileSystem->FileExists( szSmallFilename ) ) return;
int width, height; ConversionErrorType errCode; unsigned char *pThumbnailData = ImgUtl_ReadImageAsRGBA( szFullFilename, width, height, errCode ); if ( errCode != CE_SUCCESS ) { DevMsg( "Failed to read thumbnail %s.\n", szFullFilename ); return; }
// Now convert the image to an appropriate size for preview
unsigned char *pThumbnailSmallData = NULL; const unsigned int nThumbnailSmallWidth = THUMBNAIL_SMALL_WIDTH; const unsigned int nThumbnailSmallHeight = THUMBNAIL_SMALL_HEIGHT;
if ( !ResizeRGBAImage( pThumbnailData, (width*height*4), width, height, &pThumbnailSmallData, nThumbnailSmallWidth, nThumbnailSmallHeight ) ) { DevMsg( "Failed to resize small thumbnail %s.\n", szSmallFilename ); free( pThumbnailData ); return; }
if ( ImgUtl_WriteRGBAToJPEG( pThumbnailSmallData, nThumbnailSmallWidth, nThumbnailSmallHeight, szSmallFilename ) != CE_SUCCESS ) { DevMsg( "Failed to write small thumbnail %s.\n", szSmallFilename ); }
free( pThumbnailData ); free( pThumbnailSmallData ); }
//-----------------------------------------------------------------------------
// Purpose: Validate, resize and convert a RGBA to a properly formatted size
//-----------------------------------------------------------------------------
bool CUGCFileRequest::ResizeRGBAImage( const unsigned char *pData, unsigned int nDataSize, unsigned int nWidth, unsigned int nHeight, unsigned char **pDataOut, unsigned int nNewWidth, unsigned int nNewHeight ) { // Find out how to squish the image to fit within our necessary borders
float flFrameRatio = ( (float) nNewWidth / (float) nNewWidth ); float flSourceRatio = ( (float) nWidth / (float) nHeight ); unsigned int nScaleWidth; unsigned int nScaleHeight;
if ( flSourceRatio < flFrameRatio ) { nScaleWidth = nNewWidth; nScaleHeight = ( nNewWidth / flSourceRatio ); } else if ( flSourceRatio > flFrameRatio ) { nScaleWidth = ( nNewHeight * flSourceRatio ); nScaleHeight = nNewHeight; } else { nScaleWidth = nNewWidth; nScaleHeight = nNewHeight; }
// Allocate a buffer to hold the scaled image
unsigned char *pScaleBuf = (unsigned char *) malloc( nScaleWidth * nScaleHeight * 4 ); if ( pScaleBuf == NULL ) return false;
// FIXME: Combine these helper functions into one operation, rather than multiple!
// Scale the image to the proper size
if ( ImgUtl_StretchRGBAImage( pData, nWidth, nHeight, pScaleBuf, nScaleWidth, nScaleHeight ) == CE_SUCCESS ) { // Allocate a buffer to pad this image out to
*pDataOut = (unsigned char *) malloc( nNewWidth * nNewHeight * 4 ); if ( *pDataOut == NULL ) { free( pScaleBuf ); return false; }
// Calc the offset for the image to be centered after cropping
unsigned int cropX = ( nScaleWidth - nNewWidth ) / 2; unsigned int cropY = ( nScaleHeight - nNewHeight ) / 2;
// Crop the image down to size
if ( ImgUtl_CropRGBA( cropX, cropY, nScaleWidth, nScaleHeight, nNewWidth, nNewHeight, pScaleBuf, *pDataOut ) != CE_SUCCESS ) { free( *pDataOut ); return false; } }
// Release it!
free( pScaleBuf );
return true; } #endif
//
// Sets the file request into an error state
//
UGCFileRequestStatus_t CUGCFileRequest::ThrowError( const char *lpszFormat, ... ) { va_list marker; va_start( marker, lpszFormat ); Q_vsnprintf( m_szErrorText, sizeof( m_szErrorText ), lpszFormat, marker ); va_end( marker );
#ifdef LOG_FILEREQUEST_PROGRESS
Log_Warning( LOG_WORKSHOP, "%s", m_szErrorText ); #endif // LOG_FILEREQUEST_PROGRESS
m_UGCStatus = UGCFILEREQUEST_ERROR;
return m_UGCStatus; }
#endif // !NO_STEAM
//-----------------------------------------------------------------------------
// Purpose: Same as V_ComposeFilename but can deal with NULL pointers for the directory (meaning non-existant)
//-----------------------------------------------------------------------------
void V_SafeComposeFilename( const char *pPathIn, const char *pFilenameIn, char *pDest, size_t nDestSize ) { // If we've passed in a directory, then start with it
if ( pPathIn != NULL ) { V_strncpy( pDest, pPathIn, nDestSize ); V_FixSlashes( pDest ); V_AppendSlash( pDest, nDestSize ); } else { // Make sure we're clear
pDest[0] = '\0'; }
if ( pFilenameIn != NULL ) { // Tack on the filename and fix slashes
V_strncat( pDest, pFilenameIn, nDestSize, COPY_ALL_CHARACTERS ); V_FixSlashes( pDest ); } }
bool UnzipFile( const char* szPathToZipFile, const char* szOutputDir /*= NULL*/) { char outPath[MAX_PATH]; if ( szOutputDir ) { V_strcpy_safe( outPath, szOutputDir ); } else { V_ExtractFilePath( szPathToZipFile, outPath, sizeof(outPath) ); }
bool bSuccess = false; HZIP hz = OpenZip( (void*)szPathToZipFile, 0, ZIP_FILENAME ); if ( hz ) { ZIPENTRY ze; ZRESULT zr = GetZipItem( hz, -1, &ze ); if ( zr == ZR_OK ) { uint32 count = ze.index; uint32 successCount = 0; for ( uint32 i = 0; i < count; ++i ) { if ( ZR_OK == GetZipItem( hz, i, &ze ) ) { char dest[MAX_PATH]; V_ComposeFileName( outPath, ze.name, dest, sizeof(dest) ); zr = UnzipItem( hz, i, (void*)dest, 0, ZIP_FILENAME ); if ( zr == ZR_OK ) { successCount++; } else { char errorBuf[256]; FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) ); Warning( "Failed unzipping entry '%s' in zip file '%s'. Reason: %s \n", ze.name, szPathToZipFile, errorBuf ); } } }
bSuccess = count == successCount; } else { char errorBuf[256]; FormatZipMessage( zr, errorBuf, sizeof( errorBuf ) ); Warning( "Failed to get count of items in zip file '%s'. Reason: %s \n", szPathToZipFile, errorBuf ); } CloseZip( hz ); } else { char errorBuf[256]; FormatZipMessage( ZR_RECENT, errorBuf, sizeof(errorBuf ) ); Warning( "Failed to open zip file '%s'. Reason: %s\n", szPathToZipFile, errorBuf ); }
return bSuccess; }
bool UGCUtil_IsOfficialMap( PublishedFileId_t id ) { /** Removed for partner depot **/ return false; }
//-----------------------------------------------------------------------------
// Purpose: Get the local file name on disk, accounting for target directories and filenames
//-----------------------------------------------------------------------------
void CUGCFileRequest::GetLocalFileName( char *pDest, size_t strSize ) { if ( m_szTargetDirectory[0] == '\0' ) { V_strncpy( pDest, GetFilename(), strSize ); } else { V_snprintf( pDest, strSize, "%s/%s", m_szTargetDirectory, GetFilename() ); } }
//-----------------------------------------------------------------------------
// Purpose: Get the local directory on disk, accounting for target directories
//-----------------------------------------------------------------------------
void CUGCFileRequest::GetLocalDirectory( char *pDest, size_t strSize ) { if ( m_szTargetDirectory[0] == '\0' ) { V_strncpy( pDest, "\0", strSize ); } else { V_strncpy( pDest, m_szTargetDirectory, strSize ); } }
|