Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
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

//========= 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;
};