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.
1112 lines
31 KiB
1112 lines
31 KiB
5 years ago
|
//========= Copyright Valve Corporation, All rights reserved. ============//
|
||
|
#include "cbase.h"
|
||
|
#include "custom_texture_cache.h"
|
||
|
#include "materialsystem/imaterialproxy.h"
|
||
|
#include "materialsystem/imaterialvar.h"
|
||
|
#include "materialsystem/itexture.h"
|
||
|
#include "pixelwriter.h"
|
||
|
#include "checksum_md5.h"
|
||
|
#include "imageutils.h"
|
||
|
#include "toolframework_client.h"
|
||
|
#include "econ_gcmessages.h"
|
||
|
#include "econ_item_inventory.h"
|
||
|
|
||
|
#include "VGuiMatSurface/IMatSystemSurface.h"
|
||
|
#include "bitmap/bitmap.h"
|
||
|
|
||
|
using namespace CustomTextureSystem;
|
||
|
|
||
|
ITexture *CustomTextureSystem::g_pPreviewCustomTexture = NULL;
|
||
|
|
||
|
CEconItemView *CustomTextureSystem::g_pPreviewEconItem = NULL;
|
||
|
|
||
|
bool CustomTextureSystem::g_pPreviewCustomTextureDirty = true;
|
||
|
|
||
|
const char CustomTextureSystem::k_rchCustomTextureFilterPreviewImageName[] = "__CustomTextureFilterPreview";
|
||
|
const char CustomTextureSystem::k_rchCustomTextureFilterPreviewTextureName[] = "vgui/__CustomTextureFilterPreview";
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
|
||
|
static ISteamRemoteStorage *GetISteamRemoteStorage()
|
||
|
{
|
||
|
return steamapicontext?steamapicontext->SteamRemoteStorage():NULL;
|
||
|
// return Steam3Client().SteamRemoteStorage();
|
||
|
}
|
||
|
|
||
|
static void CalcMD5Ascii( char *szDigestAscii, const void *data, int dataSz )
|
||
|
{
|
||
|
MD5Context_t context;
|
||
|
unsigned char digest[ MD5_DIGEST_LENGTH ];
|
||
|
MD5Init( &context );
|
||
|
MD5Update( &context, (const unsigned char *)data, dataSz );
|
||
|
MD5Final( digest, &context );
|
||
|
Q_binarytohex( digest, MD5_DIGEST_LENGTH, szDigestAscii, MD5_DIGEST_LENGTH*2+1 );
|
||
|
}
|
||
|
|
||
|
static bool BReadSteamRemoteFileToBuffer( CUtlBuffer &outBuffer, const char *pchRemoteFilename )
|
||
|
{
|
||
|
outBuffer.Purge();
|
||
|
|
||
|
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
|
||
|
if ( !pRemoteStorage )
|
||
|
return false;
|
||
|
if ( !pRemoteStorage->FileExists( pchRemoteFilename ))
|
||
|
return false;
|
||
|
int nFileSize = pRemoteStorage->GetFileSize( pchRemoteFilename );
|
||
|
if ( nFileSize <= 0 )
|
||
|
return false;
|
||
|
|
||
|
// Allocate space
|
||
|
outBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSize );
|
||
|
int nSizeRead = pRemoteStorage->FileRead( pchRemoteFilename, outBuffer.Base(), nFileSize );
|
||
|
return ( nSizeRead == nFileSize );
|
||
|
}
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Local cache of custom images. This cache contains files from the cloud, and
|
||
|
// and also a few virtual textures that are used during autioning / tweaking
|
||
|
|
||
|
/// Name of cloud-backed config file remembering the custom images that the user
|
||
|
/// has uploaded to the cloud.
|
||
|
static const char k_szCustomTextureRecentListFilename[] = "stamped_items_mru.txt";
|
||
|
|
||
|
/// Track a single entry
|
||
|
struct SCustomImageCacheEntry : private ITextureRegenerator
|
||
|
{
|
||
|
|
||
|
/// If this has been assigned a cloud ID, what is it?
|
||
|
UGCHandle_t m_hCloudID;
|
||
|
|
||
|
/// If this is one of our files, then we know the MD5.
|
||
|
/// This is empty for other people's files
|
||
|
char m_szDigestAscii[ MD5_DIGEST_LENGTH*2 + 4];
|
||
|
|
||
|
//
|
||
|
// Bookkeeping for steam downloads of UGC.
|
||
|
//
|
||
|
|
||
|
/// -1 = failure, 0 = not started, 1 = in progress, 2 = finished OK and image should be in memory
|
||
|
int m_nStatus;
|
||
|
|
||
|
/// Handle to the active download, or k_uAPICallInvalid if not active
|
||
|
SteamAPICall_t m_hDownloadApiCall;
|
||
|
|
||
|
/// Procedural texture object. We hold a reference.
|
||
|
ITexture *m_pTexture;
|
||
|
|
||
|
/// Procedurally-created material. We hold a reference.
|
||
|
IMaterial *m_pMaterial;
|
||
|
|
||
|
/// GUI texture handle. (It's bound to the material.)
|
||
|
int m_iVguiHandle;
|
||
|
|
||
|
/// The raw image
|
||
|
Bitmap_t m_image;
|
||
|
|
||
|
/// Doubly-linked list. We keep it in MRU order so we know what to eject from the cache
|
||
|
SCustomImageCacheEntry *m_pPrev;
|
||
|
SCustomImageCacheEntry *m_pNext;
|
||
|
|
||
|
SCustomImageCacheEntry()
|
||
|
: m_hCloudID(0)
|
||
|
, m_pTexture(NULL)
|
||
|
, m_nStatus(0)
|
||
|
, m_hDownloadApiCall(k_uAPICallInvalid)
|
||
|
, m_pPrev(NULL)
|
||
|
, m_pNext(NULL)
|
||
|
, m_pMaterial(NULL)
|
||
|
, m_iVguiHandle(0)
|
||
|
{
|
||
|
m_szDigestAscii[0] = '\0';
|
||
|
}
|
||
|
|
||
|
virtual ~SCustomImageCacheEntry()
|
||
|
{
|
||
|
Clear();
|
||
|
}
|
||
|
|
||
|
// Release texture / VGUI resources. This doesn't free the image we have
|
||
|
// loaded or stop any async actions that were in progress. (Use Clear())
|
||
|
void ReleaseResources()
|
||
|
{
|
||
|
if ( m_pTexture )
|
||
|
{
|
||
|
ITexture *tex = m_pTexture;
|
||
|
m_pTexture = NULL; // clear pointer first, to prevent infinite recursion
|
||
|
tex->SetTextureRegenerator( NULL );
|
||
|
tex->Release();
|
||
|
}
|
||
|
if ( m_pMaterial )
|
||
|
{
|
||
|
m_pMaterial->Release();
|
||
|
m_pMaterial = NULL;
|
||
|
}
|
||
|
if ( m_iVguiHandle != 0 )
|
||
|
{
|
||
|
g_pMatSystemSurface->DestroyTextureID( m_iVguiHandle );
|
||
|
m_iVguiHandle = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Inherited from ITextureRegenerator
|
||
|
///
|
||
|
/// Gets called when our ITextureRegenerator interface gets detached from the texture.
|
||
|
/// We should be the only ones doing this --- so that means we had better have already
|
||
|
/// cleared our texture at this point!
|
||
|
virtual void Release()
|
||
|
{
|
||
|
Assert( m_pTexture == NULL );
|
||
|
}
|
||
|
|
||
|
/// Inherited from ITextureRegenerator
|
||
|
///
|
||
|
/// The main interface function that actually supplies the texture bits
|
||
|
virtual void RegenerateTextureBits( ITexture *pTexture, IVTFTexture *pVTFTexture, Rect_t *pRect )
|
||
|
{
|
||
|
|
||
|
Assert( pVTFTexture->FrameCount() == 1 );
|
||
|
Assert( pVTFTexture->FaceCount() == 1 );
|
||
|
Assert( pTexture == m_pTexture );
|
||
|
Assert( !pTexture->IsMipmapped() );
|
||
|
|
||
|
int nWidth, nHeight, nDepth;
|
||
|
pVTFTexture->ComputeMipLevelDimensions( 0, &nWidth, &nHeight, &nDepth );
|
||
|
Assert( nDepth == 1 );
|
||
|
Assert( nWidth == m_image.Width() && nHeight == m_image.Height() );
|
||
|
|
||
|
CPixelWriter pixelWriter;
|
||
|
pixelWriter.SetPixelMemory( pVTFTexture->Format(),
|
||
|
pVTFTexture->ImageData( 0, 0, 0 ), pVTFTexture->RowSizeInBytes( 0 ) );
|
||
|
|
||
|
// !SPEED! 'Tis probably DEATHLY slow...
|
||
|
for ( int y = 0; y < nHeight; ++y )
|
||
|
{
|
||
|
pixelWriter.Seek( 0, y );
|
||
|
for ( int x = 0; x < nWidth; ++x )
|
||
|
{
|
||
|
Color c = m_image.GetColor( x, y );
|
||
|
pixelWriter.WritePixel( c.r(), c.g(), c.b(), c.a() );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void Clear()
|
||
|
{
|
||
|
ReleaseResources();
|
||
|
m_image.Clear();
|
||
|
m_szDigestAscii[0] = '\0';
|
||
|
m_nStatus = 0;
|
||
|
m_hCloudID = 0;
|
||
|
|
||
|
// !KLUDGE! How can I clean this up properly if something is
|
||
|
// in progress?
|
||
|
m_hDownloadApiCall = k_uAPICallInvalid;
|
||
|
}
|
||
|
|
||
|
/// Poll the entry and update bookeeping if we're busy.
|
||
|
void Poll()
|
||
|
{
|
||
|
|
||
|
// We must know our cloud ID
|
||
|
if ( m_hCloudID == 0 )
|
||
|
{
|
||
|
Assert( m_hCloudID != 0 );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If texture already exists, then we are definitely done!
|
||
|
if ( m_pTexture )
|
||
|
{
|
||
|
Assert( m_nStatus == 2 );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check if we have not yet initiated anything
|
||
|
if ( m_nStatus == 0 )
|
||
|
{
|
||
|
|
||
|
// We'll need to download it.
|
||
|
// Start by assuming failure.
|
||
|
m_nStatus = -1;
|
||
|
|
||
|
// Start download
|
||
|
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
|
||
|
if ( pRemoteStorage )
|
||
|
{
|
||
|
m_hDownloadApiCall = pRemoteStorage->UGCDownload( m_hCloudID, 0 );
|
||
|
if ( m_hDownloadApiCall != k_uAPICallInvalid )
|
||
|
{
|
||
|
// Mark download as in progress
|
||
|
Msg( "Started download of cloud file %08X%08X\n", (uint32)(m_hCloudID>>32), (uint32)m_hCloudID );
|
||
|
m_nStatus = 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we're in progress, poll the result
|
||
|
if ( m_nStatus == 1 )
|
||
|
{
|
||
|
PollDownload();
|
||
|
}
|
||
|
|
||
|
// If result has completed, then fetch the texture
|
||
|
Assert( m_pTexture == NULL );
|
||
|
if ( m_nStatus == 2 && m_image.IsValid() )
|
||
|
{
|
||
|
// Generate the logical texture name
|
||
|
char rchTextureName[MAX_PATH];
|
||
|
GenerateLocalTextureName( rchTextureName );
|
||
|
|
||
|
ITexture *pTexture = NULL;
|
||
|
if ( g_pMaterialSystem->IsTextureLoaded( rchTextureName ) )
|
||
|
{
|
||
|
pTexture = g_pMaterialSystem->FindTexture( rchTextureName, TEXTURE_GROUP_VGUI );
|
||
|
pTexture->AddRef();
|
||
|
Assert( pTexture );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
pTexture = g_pMaterialSystem->CreateProceduralTexture(
|
||
|
rchTextureName,
|
||
|
TEXTURE_GROUP_VGUI,
|
||
|
k_nCustomImageSize, k_nCustomImageSize,
|
||
|
IMAGE_FORMAT_RGBA8888,
|
||
|
TEXTUREFLAGS_CLAMPS | TEXTUREFLAGS_CLAMPT | TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD
|
||
|
);
|
||
|
Assert( pTexture );
|
||
|
}
|
||
|
pTexture->SetTextureRegenerator( this ); // note carefully order of operations here. See Release()
|
||
|
m_pTexture = pTexture;
|
||
|
|
||
|
// Upload the data now
|
||
|
m_pTexture->Download();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Assert( m_nStatus < 2 );
|
||
|
Assert( !m_image.IsValid() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void PollDownload()
|
||
|
{
|
||
|
|
||
|
Assert( m_nStatus == 1 );
|
||
|
|
||
|
// Sanity check we have everything we need
|
||
|
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
|
||
|
if ( m_hDownloadApiCall == k_uAPICallInvalid || !steamapicontext || !steamapicontext->SteamUtils() || !pRemoteStorage )
|
||
|
{
|
||
|
// ???
|
||
|
Assert( m_hDownloadApiCall != k_uAPICallInvalid );
|
||
|
Assert( steamapicontext && steamapicontext->SteamUtils() );
|
||
|
Assert( pRemoteStorage );
|
||
|
m_nStatus = -1;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Poll progress
|
||
|
bool bFailed;
|
||
|
RemoteStorageDownloadUGCResult_t result;
|
||
|
if ( !steamapicontext->SteamUtils()->GetAPICallResult(m_hDownloadApiCall,
|
||
|
&result, sizeof(result), RemoteStorageDownloadUGCResult_t::k_iCallback, &bFailed) )
|
||
|
{
|
||
|
// Still busy.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Make sure we got back the file we were expecting
|
||
|
Assert( result.m_hFile == m_hCloudID );
|
||
|
|
||
|
// Clear status, mark success
|
||
|
m_hDownloadApiCall = k_uAPICallInvalid;
|
||
|
|
||
|
// Completed. Did we succeed?
|
||
|
if ( bFailed )
|
||
|
{
|
||
|
Warning( "Download of custom image file from UFS (UGC=%08X%08X) failed.\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
|
||
|
m_nStatus = -1;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Fetch file details
|
||
|
AppId_t nAppID;
|
||
|
char *pchName;
|
||
|
int32 nFileSizeInBytes = -1;
|
||
|
CSteamID steamIDOwner;
|
||
|
if ( !pRemoteStorage->GetUGCDetails( m_hCloudID, &nAppID, &pchName, &nFileSizeInBytes, &steamIDOwner )
|
||
|
|| nFileSizeInBytes <= 0 || nFileSizeInBytes >= k_nMaxCustomImageFileSize )
|
||
|
{
|
||
|
Warning( "GetUGCDetails failed? (UGC=%08X%08X nFileSizeInBytes=%d).\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID), nFileSizeInBytes );
|
||
|
m_nStatus = -1;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Load the file data
|
||
|
CUtlBuffer fileData;
|
||
|
fileData.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSizeInBytes );
|
||
|
|
||
|
// Read in the data. Phil says this is supposed to be basically a memcpy
|
||
|
// or some other fast, local operation.
|
||
|
if ( pRemoteStorage->UGCRead( m_hCloudID, fileData.Base( ), nFileSizeInBytes, 0, k_EUGCRead_ContinueReadingUntilFinished ) != nFileSizeInBytes )
|
||
|
{
|
||
|
Warning( "UGCRead failed? (UGC=%08X%08X).\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
|
||
|
m_nStatus = -1;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Parse the PNG file data
|
||
|
if ( ImgUtl_LoadPNGBitmapFromBuffer( fileData, m_image ) != CE_SUCCESS )
|
||
|
{
|
||
|
Warning( "Corrupt PNG file, UGC=%08X%08X.\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
|
||
|
m_nStatus = -1;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// We have the raw data
|
||
|
m_nStatus = 2;
|
||
|
}
|
||
|
|
||
|
int GetGuiHandle()
|
||
|
{
|
||
|
// Should never be called on entries without a cloud ID
|
||
|
if ( m_hCloudID == 0 )
|
||
|
{
|
||
|
Assert( m_hCloudID != 0 );
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Already have one?
|
||
|
if ( m_iVguiHandle != 0 )
|
||
|
{
|
||
|
return m_iVguiHandle;
|
||
|
}
|
||
|
|
||
|
// Process texture downloading, etc
|
||
|
Poll();
|
||
|
|
||
|
// If we don't have a texture yet, or don't know our logical name, then we cannot draw
|
||
|
if ( m_pTexture == NULL )
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Make a material, if we don't already have one
|
||
|
if ( m_pMaterial == NULL )
|
||
|
{
|
||
|
|
||
|
// Generate the material name
|
||
|
char rchImageName[MAX_PATH], rchMaterialName[MAX_PATH];
|
||
|
GenerateLocalImageNameBase( rchImageName );
|
||
|
Q_snprintf( rchMaterialName, MAX_PATH, "vgui/%s.mtl", rchImageName );
|
||
|
|
||
|
// Does it already exist?
|
||
|
if ( g_pMaterialSystem->IsMaterialLoaded( rchMaterialName ) )
|
||
|
{
|
||
|
m_pMaterial = g_pMaterialSystem->FindMaterial( rchMaterialName, TEXTURE_GROUP_VGUI );
|
||
|
Assert( m_pMaterial );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
|
||
|
// Fetch the texture name
|
||
|
char rchTextureName[MAX_PATH];
|
||
|
GenerateLocalTextureName( rchTextureName );
|
||
|
|
||
|
// Create dummy material KV data
|
||
|
KeyValues *pVMTKeyValues = new KeyValues( "UnlitGeneric" );
|
||
|
pVMTKeyValues->SetString( "$basetexture", rchTextureName );
|
||
|
pVMTKeyValues->SetInt( "$vertexcolor", 1 );
|
||
|
pVMTKeyValues->SetInt( "$vertexalpha", 1 );
|
||
|
pVMTKeyValues->SetInt( "$translucent", 1 );
|
||
|
|
||
|
// Create the material
|
||
|
m_pMaterial = g_pMaterialSystem->CreateMaterial(
|
||
|
rchMaterialName,
|
||
|
pVMTKeyValues
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Bind the material to a new VGUI texture object
|
||
|
m_iVguiHandle = g_pMatSystemSurface->CreateNewTextureID();
|
||
|
g_pMatSystemSurface->DrawSetTextureMaterial( m_iVguiHandle, m_pMaterial );
|
||
|
}
|
||
|
|
||
|
return m_iVguiHandle;
|
||
|
}
|
||
|
|
||
|
// Generate logical image name, with no leading materials or vgui directories
|
||
|
// nor a file extension.
|
||
|
void GenerateLocalImageNameBase( char *result ) const
|
||
|
{
|
||
|
Assert( m_hCloudID != 0 );
|
||
|
|
||
|
// Generate the local filenames. !KLUDGE! I'm not sure the platform-safe way
|
||
|
// to print a 64-bit int, so I'll just print both halves myself
|
||
|
Q_snprintf( result, 64, "cloud_custom_images/%08X%08X", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
|
||
|
}
|
||
|
|
||
|
/// Logical texture name, including "vgui" but not "materials"
|
||
|
void GenerateLocalTextureName( char *result ) const
|
||
|
{
|
||
|
char rchImageName[MAX_PATH];
|
||
|
GenerateLocalImageNameBase( rchImageName );
|
||
|
Q_snprintf( result, MAX_PATH, "vgui/%s.vtf", rchImageName );
|
||
|
}
|
||
|
|
||
|
/// Full local filename, including leading "materials" directory
|
||
|
void GenerateLocalFilename( char *result ) const
|
||
|
{
|
||
|
char szLocalTextureName[MAX_PATH];
|
||
|
GenerateLocalTextureName( szLocalTextureName );
|
||
|
|
||
|
Q_snprintf( result, MAX_PATH, "materials/%s", szLocalTextureName );
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/// Head of linked list of entries, in MRU order
|
||
|
static SCustomImageCacheEntry *mruCustomImageEntry = NULL;
|
||
|
|
||
|
/// Map of entries, indexed by cloud ID
|
||
|
typedef CUtlMap<UGCHandle_t, SCustomImageCacheEntry *, int> tCustomTextureInfoMap;
|
||
|
static tCustomTextureInfoMap g_mapCustomTextureInfoByCloudId( DefLessFunc(UGCHandle_t) );
|
||
|
|
||
|
// Remove from linked list, without deleting. The item must already be in the list
|
||
|
static void CustomTextureCache_Remove(SCustomImageCacheEntry *pEntry)
|
||
|
{
|
||
|
Assert( pEntry );
|
||
|
|
||
|
// List had better not be empty. Commence paranoia.
|
||
|
Assert( mruCustomImageEntry );
|
||
|
Assert( !mruCustomImageEntry->m_pPrev );
|
||
|
|
||
|
SCustomImageCacheEntry *p = pEntry->m_pPrev;
|
||
|
SCustomImageCacheEntry *n = pEntry->m_pNext;
|
||
|
|
||
|
// Detach from next, if we're not last
|
||
|
if ( n != NULL )
|
||
|
{
|
||
|
Assert( n->m_pPrev == pEntry);
|
||
|
n->m_pPrev = p;
|
||
|
}
|
||
|
|
||
|
// At the head?
|
||
|
if ( !p )
|
||
|
{
|
||
|
Assert( mruCustomImageEntry == pEntry );
|
||
|
mruCustomImageEntry = n;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
|
||
|
// Detach from previous
|
||
|
Assert( p->m_pNext == pEntry );
|
||
|
p->m_pNext = n;
|
||
|
}
|
||
|
|
||
|
// Clear pointers
|
||
|
pEntry->m_pPrev = pEntry->m_pNext = NULL;
|
||
|
}
|
||
|
|
||
|
// Insert the item at the head (MRU) slot. The item shouldn't
|
||
|
// already be in the list
|
||
|
static void CustomTextureCache_InsertAtHead(SCustomImageCacheEntry *pEntry)
|
||
|
{
|
||
|
Assert( pEntry );
|
||
|
Assert( !pEntry->m_pNext );
|
||
|
Assert( !pEntry->m_pPrev );
|
||
|
|
||
|
// Edge case of inserting into empty list
|
||
|
if ( mruCustomImageEntry )
|
||
|
{
|
||
|
Assert( !mruCustomImageEntry->m_pPrev );
|
||
|
mruCustomImageEntry->m_pPrev = pEntry;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Inserting into an empty list
|
||
|
}
|
||
|
|
||
|
// Do the head insertion.
|
||
|
pEntry->m_pNext = mruCustomImageEntry;
|
||
|
mruCustomImageEntry = pEntry;
|
||
|
}
|
||
|
|
||
|
// Reorder list, setting item at the head (MRU) slot. The item must already
|
||
|
// be in the list somewhere.
|
||
|
static void CustomTextureCache_SetMRU(SCustomImageCacheEntry *pEntry)
|
||
|
{
|
||
|
// Note: even if we are already at the head, go through the motions, anyway,
|
||
|
// to exercise all of the sanity checking code.
|
||
|
CustomTextureCache_Remove(pEntry);
|
||
|
CustomTextureCache_InsertAtHead(pEntry);
|
||
|
}
|
||
|
|
||
|
static SCustomImageCacheEntry *CustomTextureCache_NewEntry()
|
||
|
{
|
||
|
|
||
|
SCustomImageCacheEntry *pEntry = new SCustomImageCacheEntry;
|
||
|
|
||
|
// Go ahead and put us at the head
|
||
|
CustomTextureCache_InsertAtHead(pEntry);
|
||
|
|
||
|
// Return the new entry
|
||
|
return pEntry;
|
||
|
}
|
||
|
|
||
|
static SCustomImageCacheEntry *CustomTextureCache_FindOrAddByCloudId( UGCHandle_t ugcHandle )
|
||
|
{
|
||
|
|
||
|
// Locate the bookeeping entry, if one exists
|
||
|
int idx = g_mapCustomTextureInfoByCloudId.Find( ugcHandle );
|
||
|
SCustomImageCacheEntry *pEntry;
|
||
|
if ( g_mapCustomTextureInfoByCloudId.IsValidIndex( idx ) )
|
||
|
{
|
||
|
pEntry = g_mapCustomTextureInfoByCloudId[idx];
|
||
|
|
||
|
// We're accessing it, so move it to the head, the MRU slot
|
||
|
CustomTextureCache_SetMRU(pEntry);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Grab a new entry
|
||
|
pEntry = CustomTextureCache_NewEntry();
|
||
|
|
||
|
// Assign the cloud ID
|
||
|
pEntry->m_hCloudID = ugcHandle;
|
||
|
|
||
|
// Add it to the map by cloud ID
|
||
|
idx = g_mapCustomTextureInfoByCloudId.Insert( ugcHandle );
|
||
|
g_mapCustomTextureInfoByCloudId[idx] = pEntry;
|
||
|
}
|
||
|
|
||
|
// Return the entry
|
||
|
return pEntry;
|
||
|
}
|
||
|
|
||
|
// Locate an entry by hash create a new entry if one doesn't already exist
|
||
|
static SCustomImageCacheEntry *CustomTextureCache_FindOrAddByDigest( const char *szDigestAscii )
|
||
|
{
|
||
|
Assert( strlen(szDigestAscii) == MD5_DIGEST_LENGTH*2 );
|
||
|
|
||
|
// Brute-force linear search. This should never be called in time-critical
|
||
|
// situations
|
||
|
SCustomImageCacheEntry *pEntry = mruCustomImageEntry;
|
||
|
while ( pEntry )
|
||
|
{
|
||
|
|
||
|
// Match?
|
||
|
if ( !Q_stricmp(pEntry->m_szDigestAscii, szDigestAscii) )
|
||
|
{
|
||
|
|
||
|
// Found. Se at MRU and return it.
|
||
|
CustomTextureCache_SetMRU(pEntry);
|
||
|
return pEntry;
|
||
|
}
|
||
|
|
||
|
// Keep looking
|
||
|
pEntry = pEntry->m_pNext;
|
||
|
}
|
||
|
|
||
|
// Not found. Make a new entry
|
||
|
pEntry = CustomTextureCache_NewEntry();
|
||
|
V_strcpy_safe(pEntry->m_szDigestAscii, szDigestAscii);
|
||
|
return pEntry;
|
||
|
}
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
int GetCustomTextureGuiHandle( uint64 hCloudId )
|
||
|
{
|
||
|
|
||
|
// Find or create the entry
|
||
|
SCustomImageCacheEntry *pEntry = CustomTextureCache_FindOrAddByCloudId( hCloudId );
|
||
|
|
||
|
// Poll entry and return GUI handle if it's finally ready
|
||
|
return pEntry->GetGuiHandle();
|
||
|
}
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
|
||
|
class CCustomTextureOnItemProxy : public IMaterialProxy
|
||
|
{
|
||
|
public:
|
||
|
CCustomTextureOnItemProxy();
|
||
|
virtual ~CCustomTextureOnItemProxy();
|
||
|
|
||
|
virtual bool Init( IMaterial* pMaterial, KeyValues *pKeyValues );
|
||
|
virtual void OnBind( void *pC_BaseEntity );
|
||
|
virtual void Release();
|
||
|
virtual IMaterial *GetMaterial();
|
||
|
|
||
|
protected:
|
||
|
virtual void OnBindInternal( CEconItemView *pScriptItem );
|
||
|
|
||
|
private:
|
||
|
IMaterialVar *m_pBaseTextureVar;
|
||
|
ITexture *m_pOriginalTexture;
|
||
|
};
|
||
|
|
||
|
EXPOSE_INTERFACE( CCustomTextureOnItemProxy, IMaterialProxy, "CustomSteamImageOnModel" IMATERIAL_PROXY_INTERFACE_VERSION );
|
||
|
|
||
|
CCustomTextureOnItemProxy::CCustomTextureOnItemProxy()
|
||
|
: m_pBaseTextureVar( NULL )
|
||
|
, m_pOriginalTexture( NULL )
|
||
|
{
|
||
|
|
||
|
}
|
||
|
|
||
|
CCustomTextureOnItemProxy::~CCustomTextureOnItemProxy()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
bool CCustomTextureOnItemProxy::Init( IMaterial *pMaterial, KeyValues *pKeyValues )
|
||
|
{
|
||
|
Release();
|
||
|
|
||
|
bool found = false;
|
||
|
m_pBaseTextureVar = pMaterial->FindVar( "$basetexture", &found );
|
||
|
if ( !found )
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// No! Don't do this until, because the material/texture might
|
||
|
// not have been cached. If we call this, it causes the material
|
||
|
// to try to get cached, but instead of loading the texture
|
||
|
// synchronously, it just goes into a queue, and we get the error
|
||
|
// texture instead. We'll just defer it until later when we know
|
||
|
// for sure that everything is ready to go.
|
||
|
//m_pOriginalTexture = m_pBaseTextureVar->GetTextureValue();
|
||
|
//if ( m_pOriginalTexture )
|
||
|
//{
|
||
|
// m_pOriginalTexture->AddRef();
|
||
|
//}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void CCustomTextureOnItemProxy::OnBind( void *pC_BaseEntity )
|
||
|
{
|
||
|
if ( pC_BaseEntity )
|
||
|
{
|
||
|
CEconItemView *pScriptItem = NULL;
|
||
|
IClientRenderable *pRend = (IClientRenderable *)pC_BaseEntity;
|
||
|
C_BaseEntity *pEntity = pRend->GetIClientUnknown()->GetBaseEntity();
|
||
|
if ( pEntity )
|
||
|
{
|
||
|
CEconEntity *pItem = dynamic_cast< CEconEntity* >( pEntity );
|
||
|
if ( pItem )
|
||
|
{
|
||
|
pScriptItem = pItem->GetAttributeContainer()->GetItem();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Proxy data can be a script created item itself, if we're in a vgui CModelPanel
|
||
|
pScriptItem = dynamic_cast< CEconItemView* >( pRend );
|
||
|
}
|
||
|
if ( pScriptItem )
|
||
|
{
|
||
|
OnBindInternal( pScriptItem );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CCustomTextureOnItemProxy::Release()
|
||
|
{
|
||
|
if ( m_pOriginalTexture )
|
||
|
{
|
||
|
m_pOriginalTexture->Release();
|
||
|
m_pOriginalTexture = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
IMaterial *CCustomTextureOnItemProxy::GetMaterial()
|
||
|
{
|
||
|
return m_pBaseTextureVar->GetOwningMaterial();
|
||
|
}
|
||
|
|
||
|
void CCustomTextureOnItemProxy::OnBindInternal( CEconItemView *pScriptItem )
|
||
|
{
|
||
|
if ( !m_pBaseTextureVar || !m_pBaseTextureVar->IsTexture() )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Snag the original texture object the first time.
|
||
|
// And make sure we're 100% ready to go.
|
||
|
if ( m_pOriginalTexture == NULL )
|
||
|
{
|
||
|
m_pOriginalTexture = m_pBaseTextureVar->GetTextureValue();
|
||
|
if ( m_pOriginalTexture == NULL )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
if ( m_pOriginalTexture->IsError() )
|
||
|
{
|
||
|
m_pOriginalTexture = NULL;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Success! Let's hang on to this guy
|
||
|
m_pOriginalTexture->AddRef();
|
||
|
}
|
||
|
ITexture *texture = m_pOriginalTexture;
|
||
|
|
||
|
// Fetch the UGC handle from the item
|
||
|
UGCHandle_t ugcHandle = pScriptItem->GetCustomUserTextureID();
|
||
|
|
||
|
// Are we in a preview window?
|
||
|
if ( pScriptItem == g_pPreviewEconItem ) // !KLUDGE!
|
||
|
{
|
||
|
Assert( g_pPreviewCustomTexture );
|
||
|
if ( g_pPreviewCustomTexture )
|
||
|
{
|
||
|
texture = g_pPreviewCustomTexture;
|
||
|
|
||
|
// Re-fetch the bits if necessary
|
||
|
if ( g_pPreviewCustomTextureDirty )
|
||
|
{
|
||
|
g_pPreviewCustomTexture->Download();
|
||
|
Assert( !g_pPreviewCustomTextureDirty );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (ugcHandle != 0)
|
||
|
{
|
||
|
|
||
|
SCustomImageCacheEntry *pEntry = CustomTextureCache_FindOrAddByCloudId(ugcHandle);
|
||
|
pEntry->Poll();
|
||
|
texture = pEntry->m_pTexture; // might be NULL if texture isn't ready yet
|
||
|
}
|
||
|
|
||
|
if ( texture )
|
||
|
{
|
||
|
m_pBaseTextureVar->SetTextureValue( texture );
|
||
|
}
|
||
|
|
||
|
if ( ToolsEnabled() )
|
||
|
{
|
||
|
ToolFramework_RecordMaterialParams( GetMaterial() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// The custom texture cache needs to init/shutdown and get some frame ticking
|
||
|
//-----------------------------------------------------------------------------
|
||
|
class CCustomTextureToolCache : public CBaseGameSystemPerFrame
|
||
|
{
|
||
|
public:
|
||
|
CCustomTextureToolCache() {}
|
||
|
virtual ~CCustomTextureToolCache() {}
|
||
|
|
||
|
//
|
||
|
// CAutoGameSystemPerFrame overrides
|
||
|
//
|
||
|
virtual char const *Name()
|
||
|
{
|
||
|
return "CCustomTextureToolCache";
|
||
|
}
|
||
|
virtual bool Init()
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
virtual void Shutdown()
|
||
|
{
|
||
|
|
||
|
// Destroy all the cache entries
|
||
|
SCustomImageCacheEntry *pEntry = mruCustomImageEntry;
|
||
|
mruCustomImageEntry = NULL;
|
||
|
while ( pEntry != NULL )
|
||
|
{
|
||
|
SCustomImageCacheEntry *pNext = pEntry->m_pNext;
|
||
|
delete pEntry;
|
||
|
pEntry = pNext;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// At level shutdown, release all of our GPU resources.
|
||
|
// We'll still hang on to the bitmap data, since it isn't
|
||
|
// that large and is in regular virtual memory which is easily
|
||
|
// swapped out if stale. But the video RAM we want to be more
|
||
|
// agressive at cleaning out.
|
||
|
virtual void LevelShutdownPreEntity()
|
||
|
{
|
||
|
// Destroy all the cache entries
|
||
|
for ( SCustomImageCacheEntry *pEntry = mruCustomImageEntry ; pEntry ; pEntry = pEntry->m_pNext )
|
||
|
{
|
||
|
pEntry->ReleaseResources();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CAutoGameSystemPerFrame defines different stuff depending on which DLL we're building
|
||
|
#ifdef CLIENT_DLL
|
||
|
|
||
|
// Do our frame-time processing after rendering
|
||
|
virtual void PostRender()
|
||
|
{
|
||
|
// !FIXME! Here's where we should scan the list and eject
|
||
|
// entries that haven't been used recently to limit the
|
||
|
// hardware resources we're using.
|
||
|
}
|
||
|
#else
|
||
|
// This file shouldn't be compiled outside of client.dll. Right?
|
||
|
#error "Say what?"
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
static CCustomTextureToolCache s_CustomTextureToolCache;
|
||
|
IGameSystem *CustomTextureToolCacheGameSystem()
|
||
|
{
|
||
|
return &s_CustomTextureToolCache;
|
||
|
}
|
||
|
|
||
|
|
||
|
CApplyCustomTextureJob::CApplyCustomTextureJob( itemid_t nToolItemID, itemid_t nSubjectItemID, const void *pPNGData, int nPNGDataBytes )
|
||
|
: GCSDK::CGCClientJob( GCClientSystem()->GetGCClient() )
|
||
|
, m_nToolItemID( nToolItemID )
|
||
|
, m_nSubjectItemID( nSubjectItemID )
|
||
|
, m_hCloudID( 0 )
|
||
|
{
|
||
|
m_chRemoteStorageName[0] = '\0';
|
||
|
m_bufPNGData.Put( pPNGData, nPNGDataBytes );
|
||
|
}
|
||
|
|
||
|
bool CApplyCustomTextureJob::BYieldingRunGCJob()
|
||
|
{
|
||
|
YieldingRunJob();
|
||
|
CleanUp();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void CApplyCustomTextureJob::CleanUp()
|
||
|
{
|
||
|
// If we had a cloud file, delete it from the logical
|
||
|
// cloud filespace. We are using the cloud system really just
|
||
|
// to get the file into the UGC system and get a handle to it.
|
||
|
// But once it's up there, it really isn't this user. They will
|
||
|
// fetch it by UGC handle just like any other user. They paid
|
||
|
// for this action, and we don't want it taking up any of their
|
||
|
// quota.
|
||
|
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
|
||
|
if ( pRemoteStorage && m_chRemoteStorageName[0] != '\0' )
|
||
|
{
|
||
|
pRemoteStorage->FileDelete( m_chRemoteStorageName );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
EResult CApplyCustomTextureJob::YieldingRunJob()
|
||
|
{
|
||
|
EResult result = YieldingFindFileIncacheOrUploadFileToCDN();
|
||
|
if ( result != k_EResultOK )
|
||
|
{
|
||
|
return result;
|
||
|
}
|
||
|
Assert( m_hCloudID != 0 );
|
||
|
|
||
|
result = YieldingApplyTool();
|
||
|
if ( result != k_EResultOK )
|
||
|
{
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
// OK!
|
||
|
return k_EResultOK;
|
||
|
}
|
||
|
|
||
|
EResult CApplyCustomTextureJob::YieldingFindFileIncacheOrUploadFileToCDN()
|
||
|
{
|
||
|
|
||
|
int nFileSize = m_bufPNGData.TellPut();
|
||
|
Assert( nFileSize <= k_nMaxCustomImageFileSize ); // what the heck is out image converter doing?!
|
||
|
|
||
|
// Generate the hash
|
||
|
char szDigestAscii[ MD5_DIGEST_LENGTH*2 + 4];
|
||
|
CalcMD5Ascii( szDigestAscii, m_bufPNGData.Base(), nFileSize );
|
||
|
|
||
|
// Find or create an existing cache entry
|
||
|
SCustomImageCacheEntry *pSelectedCacheEntry = CustomTextureCache_FindOrAddByDigest( szDigestAscii );
|
||
|
|
||
|
KeyValuesAD pkvMruFile( "StampedItems" );
|
||
|
|
||
|
{
|
||
|
// Load up list of images recently used and uploaded
|
||
|
CUtlBuffer listFileData;
|
||
|
listFileData.SetBufferType( true, true );
|
||
|
if ( BReadSteamRemoteFileToBuffer( listFileData, k_szCustomTextureRecentListFilename ) )
|
||
|
{
|
||
|
if ( !pkvMruFile->LoadFromBuffer( k_szCustomTextureRecentListFilename, listFileData ) )
|
||
|
{
|
||
|
pkvMruFile->Clear();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
KeyValues *pkvMruUploadedImages = pkvMruFile->FindKey( "Uploaded", true );
|
||
|
|
||
|
// !FIXME! Check for duplicates!
|
||
|
|
||
|
// Make sure we are ready
|
||
|
Assert( pSelectedCacheEntry != NULL );
|
||
|
Assert( pSelectedCacheEntry->m_hCloudID == 0 );
|
||
|
Assert( strlen(pSelectedCacheEntry->m_szDigestAscii) == MD5_DIGEST_LENGTH*2 );
|
||
|
Assert( m_bufPNGData.TellPut() > 0 );
|
||
|
|
||
|
// Generate filename in the cloud file space. Each user has their own
|
||
|
// namespace, and Phil requested that we keep the filenames simple
|
||
|
// and easily optimizeable by string table. (I.e. don't use the
|
||
|
// hash or something else)
|
||
|
//
|
||
|
// We *could* just always use the same filename, and each file would
|
||
|
// be its own "version." But that doesn't seem to be the proper
|
||
|
// spirit of the cloud system. So I'll just use a simple integer name,
|
||
|
// based on how many images they have uploaded. It isn't critical what
|
||
|
// this logical filename is, because once the GC gets a message to tag the file,
|
||
|
// that UGC ID should always refer to that version of the file and can never
|
||
|
// be changed or deleted, even if we reuse the filename.
|
||
|
int iFileIndex = 1;
|
||
|
KeyValues *pKey;
|
||
|
for ( pKey = pkvMruUploadedImages->GetFirstTrueSubKey() ; pKey ; pKey = pKey->GetNextTrueSubKey() )
|
||
|
{
|
||
|
int index = atoi(pKey->GetName());
|
||
|
iFileIndex = MAX( iFileIndex, index+1 );
|
||
|
}
|
||
|
Q_snprintf( m_chRemoteStorageName, sizeof( m_chRemoteStorageName ), "my_custom_images/%d.png", iFileIndex );
|
||
|
|
||
|
// Write the local copy of the file
|
||
|
Msg( "Saving %s to cloud....\n", m_chRemoteStorageName );
|
||
|
ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
|
||
|
if ( !pRemoteStorage || !pRemoteStorage->FileWrite( m_chRemoteStorageName, m_bufPNGData.Base(), m_bufPNGData.TellPut() ) )
|
||
|
{
|
||
|
Warning( "Failed to save local copy of custom image %s\n", m_chRemoteStorageName);
|
||
|
return k_EResultFail;
|
||
|
}
|
||
|
|
||
|
// Share it. This initiates the upload to cloud
|
||
|
Msg( "Starting upload of %s to UFS....\n", m_chRemoteStorageName );
|
||
|
SteamAPICall_t hFileShareApiCall = pRemoteStorage->FileShare( m_chRemoteStorageName );
|
||
|
if ( hFileShareApiCall == k_uAPICallInvalid )
|
||
|
{
|
||
|
return k_EResultFail;
|
||
|
}
|
||
|
|
||
|
bool bFailed;
|
||
|
RemoteStorageFileShareResult_t shareResult;
|
||
|
while ( !steamapicontext->SteamUtils()->GetAPICallResult(hFileShareApiCall,
|
||
|
&shareResult, sizeof(shareResult), RemoteStorageFileShareResult_t::k_iCallback, &bFailed) )
|
||
|
{
|
||
|
BYield();
|
||
|
}
|
||
|
|
||
|
if ( bFailed || shareResult.m_eResult != k_EResultOK )
|
||
|
{
|
||
|
Warning( "Custom texture uploaded to cloud FAILED\n" );
|
||
|
return k_EResultFail;
|
||
|
}
|
||
|
|
||
|
Msg( "Custom texture uploaded to cloud completed OK, assigned UGC ID %08X%08X\n", (uint32)(shareResult.m_hFile >> 32), (uint32)(shareResult.m_hFile) );
|
||
|
|
||
|
// Remember the handle to the cloud file
|
||
|
m_hCloudID = pSelectedCacheEntry->m_hCloudID = shareResult.m_hFile;
|
||
|
|
||
|
// Update the MRU list
|
||
|
pKey = pkvMruUploadedImages->GetFirstTrueSubKey();
|
||
|
while ( pKey )
|
||
|
{
|
||
|
int index = atoi(pKey->GetName());
|
||
|
int mruValue = pKey->GetInt( "mru", 0 );
|
||
|
const char *entryDigsetAscii = pKey->GetString("md5", "");
|
||
|
UGCHandle_t ugcID = pKey->GetUint64( "ugcid", 0);
|
||
|
if ( index <= 0 || mruValue <= 0 || strlen(entryDigsetAscii) != MD5_DIGEST_LENGTH*2 || ugcID == 0 )
|
||
|
{
|
||
|
// Bah! Bogus data!
|
||
|
Assert(false);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Is this the one they selected?
|
||
|
if ( ugcID == pSelectedCacheEntry->m_hCloudID )
|
||
|
{
|
||
|
|
||
|
// This *can* happen if the list file gets lost and they reuse an image. It means we are wasting
|
||
|
// some of their cloud quota, but should be rare, and it's harmless.
|
||
|
Assert( !Q_stricmp(entryDigsetAscii, pSelectedCacheEntry->m_szDigestAscii) );
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
pKey = pKey->GetNextTrueSubKey();
|
||
|
}
|
||
|
|
||
|
// Found it?
|
||
|
int oldIndex = 0x7fffffff;
|
||
|
if ( pKey )
|
||
|
{
|
||
|
|
||
|
// Renumber them in MRU order
|
||
|
oldIndex = pKey->GetInt( "mru", 1 );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
|
||
|
// Create a new key
|
||
|
pKey = pkvMruUploadedImages->CreateNewKey();
|
||
|
|
||
|
// Remember hash and cloud file location in subkeys
|
||
|
pKey->SetString( "md5", pSelectedCacheEntry->m_szDigestAscii );
|
||
|
pKey->SetUint64( "ugcid", pSelectedCacheEntry->m_hCloudID );
|
||
|
//pKey->SetString( "remoteStorageName", m_chSelectedRemoteStorageNameBase );
|
||
|
}
|
||
|
for ( KeyValues *p = pkvMruUploadedImages->GetFirstTrueSubKey() ; p ; p = p->GetNextTrueSubKey() )
|
||
|
{
|
||
|
if ( p != pKey )
|
||
|
{
|
||
|
int mruValue = p->GetInt( "mru", 0 );
|
||
|
Assert( mruValue > 0 );
|
||
|
if (mruValue < oldIndex)
|
||
|
{
|
||
|
p->SetInt( "mru", mruValue+1 );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pKey->SetInt( "mru", 1);
|
||
|
|
||
|
// Re-save the cloud-backed MRU list file
|
||
|
Msg( "Saving MRU list file %s\n", k_szCustomTextureRecentListFilename );
|
||
|
if ( pRemoteStorage )
|
||
|
{
|
||
|
CUtlBuffer listFileData;
|
||
|
listFileData.SetBufferType( true, true );
|
||
|
pkvMruFile->RecursiveSaveToFile( listFileData, 0 );
|
||
|
pRemoteStorage->FileWrite( k_szCustomTextureRecentListFilename, listFileData.Base(), listFileData.TellPut() );
|
||
|
}
|
||
|
|
||
|
return k_EResultOK;
|
||
|
}
|
||
|
|
||
|
EResult CApplyCustomTextureJob::YieldingApplyTool()
|
||
|
{
|
||
|
|
||
|
Msg( "Sending tool request to GC.\n" );
|
||
|
|
||
|
// At this point, we need to know the cloud ID and hash of the image we are applying
|
||
|
Assert( m_hCloudID != 0 );
|
||
|
|
||
|
// Send the message to the GC
|
||
|
GCSDK::CGCMsg< MsgGCCustomizeItemTexture_t > msg( k_EMsgGCCustomizeItemTexture );
|
||
|
msg.Body().m_unToolItemID = m_nToolItemID;
|
||
|
msg.Body().m_unSubjectItemID = m_nSubjectItemID;
|
||
|
msg.Body().m_unImageUGCHandle = m_hCloudID;
|
||
|
|
||
|
GCSDK::CGCMsg<MsgGCStandardResponse_t> msgReply;
|
||
|
if ( !BYldSendMessageAndGetReply( msg, 10, &msgReply, k_EMsgGCCustomizeItemTextureResponse ) )
|
||
|
{
|
||
|
Warning( "Customize texture tool failed: Did not get reply from GC\n" );
|
||
|
return k_EResultTimeout;
|
||
|
}
|
||
|
|
||
|
// OK!
|
||
|
InventoryManager()->ShowItemsPickedUp( true );
|
||
|
return k_EResultOK;
|
||
|
};
|
||
|
|