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

1096 lines
34 KiB
C++

//========= 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 );
}
}