summaryrefslogtreecommitdiff
path: root/game/shared/workshop
diff options
context:
space:
mode:
Diffstat (limited to 'game/shared/workshop')
-rw-r--r--game/shared/workshop/ugc_utils.cpp415
-rw-r--r--game/shared/workshop/ugc_utils.h138
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