diff options
Diffstat (limited to 'game/shared/workshop')
| -rw-r--r-- | game/shared/workshop/ugc_utils.cpp | 415 | ||||
| -rw-r--r-- | game/shared/workshop/ugc_utils.h | 138 |
2 files changed, 553 insertions, 0 deletions
diff --git a/game/shared/workshop/ugc_utils.cpp b/game/shared/workshop/ugc_utils.cpp new file mode 100644 index 0000000..8d9be36 --- /dev/null +++ b/game/shared/workshop/ugc_utils.cpp @@ -0,0 +1,415 @@ +//========= 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; +} diff --git a/game/shared/workshop/ugc_utils.h b/game/shared/workshop/ugc_utils.h new file mode 100644 index 0000000..1e24bb3 --- /dev/null +++ b/game/shared/workshop/ugc_utils.h @@ -0,0 +1,138 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Utility helper functions for dealing with UGC files +// +//==========================================================================// + +#ifndef UGC_UTILS_H +#define UGC_UTILS_H + +#include "utlbuffer.h" +#include "filesystem.h" +#include "steam/steam_api.h" + +#include "dbg.h" + +// All UGC files are assumed to be on this path by default +#define UGC_PATHID "DEFAULT_WRITE_PATH" + +// This will log the UGC file requests as they're serviced +// #define LOG_FILEREQUEST_PROGRESS + +// Enable verbose debug spew to DevMsg +// #define UGC_DEBUG + +#define UGCMsg(...) Msg("[UGC] " __VA_ARGS__) +#define UGCWarning(...) Warning("[UGC] " __VA_ARGS__) + +#ifdef UGC_DEBUG +#define UGCDebug(...) DevMsg("[UGC Debug] " __VA_ARGS__) +#else // UGC_DEBUG +#define UGCDebug(...) +#endif // UGC_DEBUG + +ISteamUGC *GetSteamUGC(); +ISteamRemoteStorage *GetSteamRemoteStorage(); + +// Consistently set/get modified/access timestamps for UGC files. +bool UGC_SetFileTime( const char *pFileRelativePath, RTime32 uTimestamp ); + +// Simulates stalling of the file IO for testing +// #define FILEREQUEST_IO_STALL +#define FILEREQUEST_IO_STALL_DELAY 1.0f // Seconds + +enum UGCFileRequestStatus_t +{ + UGCFILEREQUEST_ERROR = -1, // An error occurred while processing the file operation + UGCFILEREQUEST_READY, // File request is ready to do work + UGCFILEREQUEST_DOWNLOADING, // Currently downloading a file + UGCFILEREQUEST_DOWNLOAD_WRITING, // Async write of the downloaded file to the disc + UGCFILEREQUEST_UPLOADING, // Currently uploading a file + UGCFILEREQUEST_FINISHED // Operation complete, no work waiting +}; + +#ifdef FILEREQUEST_IO_STALL +enum +{ + FILEREQUEST_STALL_NONE, + FILEREQUEST_STALL_DOWNLOAD, // Download from UGC server + FILEREQUEST_STALL_WRITE, // Write to disc +}; +#endif // FILEREQUEST_IO_STALL + + +// FIXME(johns): This is superseded by the newer CUGCSyncedFile. Once the +// remaining users of this are migrated it should be nuked. +class CUGCFileRequest +{ +public: + CUGCFileRequest( void ); + ~CUGCFileRequest( void ); + + UGCFileRequestStatus_t StartDownload( UGCHandle_t hFileHandle, const char *lpszTargetDirectory = NULL, const char *lpszTargetFilename = NULL ); + UGCFileRequestStatus_t StartUpload( CUtlBuffer &buffer, const char *lpszFilename ); + UGCFileRequestStatus_t Update( void ); + UGCFileRequestStatus_t GetStatus( void ) const { return m_UGCStatus; } + + // Accessors + const char *GetFileName( void ) { return ( m_szTargetFilename[0] == '\0' ) ? m_szFileName : m_szTargetFilename; } + const char *GetLastError( void ) const { return m_szErrorText; } + UGCHandle_t GetCloudHandle( void ) const { return m_hCloudID; } + + void GetLocalFileName( char *pDest, size_t strSize ); + void GetLocalDirectory( char *pDest, size_t strSize ); + +private: + + CCallResult<CUGCFileRequest, RemoteStorageDownloadUGCResult_t> m_callbackUGCDownload; + void Steam_OnUGCDownload( RemoteStorageDownloadUGCResult_t *pResult, bool bError ); + + CCallResult<CUGCFileRequest, RemoteStorageFileShareResult_t> m_callbackFileShare; + void Steam_OnFileShare( RemoteStorageFileShareResult_t *pResult, bool bError ); + + // + // Marks the file request as complete and frees its internal buffers + // + + void MarkCompleteAndFree( void ) + { + m_bufContents.Clear(); + m_UGCStatus = UGCFILEREQUEST_FINISHED; + } + + // + // Sets the file request into an error state + // + + UGCFileRequestStatus_t ThrowError( const char *lpszDesc ) + { + V_strncpy( m_szErrorText, lpszDesc, ARRAYSIZE(m_szErrorText) ); + Warning( "%s", m_szErrorText ); + Assert(0); + m_UGCStatus = UGCFILEREQUEST_ERROR; + + return m_UGCStatus; + } + +private: + char m_szTargetDirectory[MAX_PATH]; // If specified, the directory the file will be placed in + char m_szTargetFilename[MAX_PATH]; // If specified, this name overrides the UGC filename + char m_szFileName[MAX_PATH]; // Filename of in the cloud structure + + SteamAPICall_t m_hSteamAPICall; // Used to track Steam API calls which are non-blocking + CUtlBuffer m_bufContents; // Contents of the file once read from the cloud + UGCHandle_t m_hCloudID; // Cloud handle of this request + FSAsyncControl_t m_AsyncControl; // Handle for the async requests this class can initiate + + UGCFileRequestStatus_t m_UGCStatus; // The current status of this request + char m_szErrorText[512]; // Holds information if an error occurred + +#ifdef FILEREQUEST_IO_STALL + // Debug data + float m_flIOStallDuration; // Amount of time (in seconds) to stall all IO operations + int m_nIOStallType; // Type of stall (0 - none, 1 - download, 2 - write ) + float m_flIOStallStart; +#endif // FILEREQUEST_IO_STALL +}; + +#endif //UGC_UTILS_H |