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.
1111 lines
31 KiB
1111 lines
31 KiB
//========= 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; |
|
}; |
|
|
|
|