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.
415 lines
14 KiB
415 lines
14 KiB
//========= 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 <sys/utime.h> |
|
#elif defined(OSX) |
|
#include <utime.h> |
|
#else |
|
#include <sys/types.h> |
|
#include <utime.h> |
|
#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; |
|
}
|
|
|