//========= Copyright Valve Corporation, All rights reserved. ============// // // Utility helper functions for dealing with UGC files // //==========================================================================// #include "cbase.h" #include "steam/steam_api.h" #include "ugc_utils.h" #include "fmtstr.h" // utime() and stat() #if defined( _WIN32 ) #include #elif defined(OSX) #include #else #include #include #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ISteamUGC *GetSteamUGC() { #ifdef GAME_DLL // Use steamgameserver context if this isn't a client/listenserver. // While we can use steamgameserver in listenservers, we want to always use client-side UGC there currently. if ( engine->IsDedicatedServer() ) { return steamgameserverapicontext ? steamgameserverapicontext->SteamUGC() : NULL; } #endif return steamapicontext ? steamapicontext->SteamUGC() : NULL; } ISteamRemoteStorage *GetSteamRemoteStorage() { return steamapicontext ? steamapicontext->SteamRemoteStorage() : NULL; } //============================================================================= // // File request helper class for older ISteamRemoteStorage UGC files. // Prefer ISteamUGC when possible. // //============================================================================= //----------------------------------------------------------------------------- // Constructor //----------------------------------------------------------------------------- CUGCFileRequest::CUGCFileRequest( void ) : m_hCloudID(k_UGCHandleInvalid), m_UGCStatus(UGCFILEREQUEST_READY), m_AsyncControl(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; 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.Clear(); } //----------------------------------------------------------------------------- // Purpose: Start a download by handle //----------------------------------------------------------------------------- UGCFileRequestStatus_t CUGCFileRequest::StartDownload( UGCHandle_t hFileHandle, const char *lpszTargetDirectory /*= NULL*/, const char *lpszTargetFilename /*= NULL*/ ) { // Start with the assumption of failure m_UGCStatus = UGCFILEREQUEST_ERROR; // Start the download request SteamAPICall_t hSteamAPICall = GetSteamRemoteStorage()->UGCDownload( hFileHandle, 0 ); m_callbackUGCDownload.Set( hSteamAPICall, this, &CUGCFileRequest::Steam_OnUGCDownload ); if ( hSteamAPICall != k_uAPICallInvalid ) { #ifdef LOG_FILEREQUEST_PROGRESS Msg( "Started download of cloud file %s/%s (%08X%08X)\n", lpszTargetDirectory, lpszTargetFilename, (uint32)(hFileHandle>>32), (uint32)hFileHandle ); #endif // LOG_FILEREQUEST_PROGRESS // Mark download as in progress m_UGCStatus = UGCFILEREQUEST_DOWNLOADING; m_hCloudID = hFileHandle; // Take a target directory for the file if ( lpszTargetDirectory != NULL ) { V_strncpy( m_szTargetDirectory, lpszTargetDirectory, MAX_PATH ); } // Take a target filename for the file if ( lpszTargetFilename != NULL ) { V_strncpy( m_szTargetFilename, lpszTargetFilename, MAX_PATH ); } #ifdef FILEREQUEST_IO_STALL m_flIOStallStart = gpGlobals->curtime; #endif // FILEREQUEST_IO_STALL // Done! return m_UGCStatus; } // We were unable to start our download through the Steam API return ThrowError( "Failed to initiate download of file from cloud\n" ); } //----------------------------------------------------------------------------- // 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; // Write the local copy of the file #ifdef LOG_FILEREQUEST_PROGRESS Msg( "Saving %s to user cloud...\n", lpszFilename ); #endif // LOG_FILEREQUEST_PROGRESS ISteamRemoteStorage *pRemoteStorage = GetSteamRemoteStorage(); if ( !pRemoteStorage || !pRemoteStorage->FileWrite( lpszFilename, buffer.Base(), buffer.TellPut() ) ) return ThrowError( "Failed to write file to cloud\n" ); // 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->curtime; #endif // FILEREQUEST_IO_STALL m_UGCStatus = UGCFILEREQUEST_UPLOADING; return m_UGCStatus; } //----------------------------------------------------------------------------- // Purpose: FileShare complete for a file request //----------------------------------------------------------------------------- void CUGCFileRequest::Steam_OnFileShare( RemoteStorageFileShareResult_t *pResult, bool bError ) { if ( bError ) { ThrowError( "Upload of file to Steam cloud failed\n" ); return; } #ifdef LOG_FILEREQUEST_PROGRESS Msg( "Custom map uploaded to cloud completed OK, assigned UGC ID %08X%08X\n", (uint32)(pResult->m_hFile >> 32), (uint32)(pResult->m_hFile) ); #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( "Download of file from cloud failed!\n" ); 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 = GetSteamRemoteStorage(); if ( !pRemoteStorage->GetUGCDetails( m_hCloudID, &nAppID, &pchName, &nFileSizeInBytes, &steamIDOwner ) || nFileSizeInBytes <= 0 ) { ThrowError( "Unable to retrieve cloud file info from Steam\n" ); return; } // Allocate a temporary buffer m_bufContents.Clear(); m_bufContents.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSizeInBytes ); // Read in the data if ( pRemoteStorage->UGCRead( m_hCloudID, m_bufContents.Base( ), nFileSizeInBytes, 0, k_EUGCRead_ContinueReadingUntilFinished ) != nFileSizeInBytes ) { ThrowError( "Failed call to UGCRead on cloud file\n" ); return; } // Save our name V_strncpy( m_szFileName, pchName, sizeof(m_szFileName) ); // Take this as our target if we haven't specified one if ( m_szTargetFilename[0] == '\0' ) { V_strncpy( m_szTargetFilename, pchName, sizeof(m_szTargetFilename) ); } #ifdef LOG_FILEREQUEST_PROGRESS Msg( "Read file %s/%s (%08X%08X)\n", m_szTargetDirectory, m_szTargetFilename, (uint32)(m_hCloudID>>32), (uint32)m_hCloudID ); #endif // LOG_FILEREQUEST_PROGRESS // FIXME: Is this already in scope? // Done downloading, so commit it to the local disc const char *lpszFilename = V_UnqualifiedFileName( GetFileName() ); char szLocalFilename[MAX_PATH]; // Make sure the directory exists if we're creating one if ( m_szTargetDirectory[0] != '\0' ) { V_snprintf( szLocalFilename, sizeof(szLocalFilename), "%s/%s", m_szTargetDirectory, lpszFilename ); g_pFullFileSystem->CreateDirHierarchy( m_szTargetDirectory, "DEFAULT_WRITE_PATH" ); } else { V_snprintf( szLocalFilename, sizeof(szLocalFilename), "%s", lpszFilename ); /* char szDirectory[MAX_PATH]; Q_FileBase( GetFileName(), szDirectory, sizeof(szDirectory) ); g_pFullFileSystem->CreateDirHierarchy( szDirectory, "DEFAULT_WRITE_PATH" ); */ } // Async write this to disc with monitoring if ( g_pFullFileSystem->AsyncWrite( szLocalFilename, m_bufContents.Base(), m_bufContents.TellPut(), false, false, &m_AsyncControl ) < 0 ) { // Async write failed immediately! ThrowError( CFmtStr( "Async write of downloaded file %s failed\n", szLocalFilename ) ); return; } #ifdef LOG_FILEREQUEST_PROGRESS Msg( "Async write started for %s (%08X%08X)\n", szLocalFilename, (uint32)(m_hCloudID>>32), (uint32)m_hCloudID ); #endif // LOG_FILEREQUEST_PROGRESS // Mark us as having started out download m_UGCStatus = UGCFILEREQUEST_DOWNLOAD_WRITING; } //----------------------------------------------------------------------------- // Purpose: Poll for status and drive the process forward //----------------------------------------------------------------------------- UGCFileRequestStatus_t CUGCFileRequest::Update( void ) { switch ( m_UGCStatus ) { // Handle the async write of the file to disc case UGCFILEREQUEST_DOWNLOAD_WRITING: { #ifdef FILEREQUEST_IO_STALL if ( m_nIOStallType == FILEREQUEST_STALL_WRITE ) { if ( ( gpGlobals->curtime - m_flIOStallStart ) < 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( "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 Msg( "Async write completed for %s/%s (%08X%08X)\n", m_szTargetDirectory, m_szTargetFilename, (uint32)(m_hCloudID>>32), (uint32)m_hCloudID ); #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( "Lost handle to async handle for downloaded file write!" ); } break; // Handle starting up a download case UGCFILEREQUEST_READY: case UGCFILEREQUEST_DOWNLOADING: case UGCFILEREQUEST_UPLOADING: case UGCFILEREQUEST_FINISHED: 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::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 ); } } //----------------------------------------------------------------------------- // Purpose: Sets the modified/access times of a file, taking care to avoid a win32 CRT bug. //----------------------------------------------------------------------------- bool UGC_SetFileTime( const char *pFileRelativePath, RTime32 uTimestamp ) { char chFullFilePathForTimestamp[ MAX_PATH ] = {0}; char const *pchFullPath = g_pFullFileSystem->RelativePathToFullPath( pFileRelativePath, UGC_PATHID, chFullFilePathForTimestamp, sizeof( chFullFilePathForTimestamp ) ); if ( pchFullPath ) { struct utimbuf tbuffer; tbuffer.modtime = tbuffer.actime = uTimestamp; int iResultCode = utime( pchFullPath, &tbuffer ); #if defined ( _WIN32 ) // In MSVC2013 and earlier, utime() incorrectly factors in daylight savings. // Prior to MSVC2013 stat() also has this bug. // This means for MSVC2013's CRT specifically, stat() stops canceling out the error and returns something // different from what utime() sets. // Seriously. // https://connect.microsoft.com/VisualStudio/feedback/details/811534/utime-sometimes-fails-to-set-the-correct-file-times-in-visual-c-2013 // Check if what we wrote is being offset, then re-set the time canceling out this offset. RTime32 unFileTimeFromStat = (RTime32)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 = (RTime32)g_pFullFileSystem->GetFileTime( pFileRelativePath, "MOD" ); Assert( unFileTimeFromStat == uTimestamp ); #endif } #endif return ( iResultCode == 0 ); } return false; }