//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //============================================================================= #include "cbase.h" #include "imageutils.h" #include "econ_controls.h" #include "econ/confirm_dialog.h" #include "game/client/iviewport.h" #include "ienginevgui.h" #include "tf_hud_mainmenuoverride.h" #include "vgui/ILocalize.h" #include "vgui/ISurface.h" #include "vgui/ISystem.h" #include "vgui_bitmappanel.h" #include #ifdef WORKSHOP_IMPORT_ENABLED #include "itemtest/itemtest.h" #include "workshop/item_import.h" #endif #include "steampublishedfiles/publish_file_dialog.h" #include "tier1/checksum_crc.h" // for hud element #include "iclientmode.h" #include "tf_gamerules.h" // memdbgon must be the last include file in a .cpp file!!! #include #define TF2_PREVIEW_IMAGE_HEIGHT 512 #define TF2_PREVIEW_IMAGE_WIDTH 512 #define COMMUNITY_DEV_HOST "http://localhost/" extern ConVar publish_file_last_dir; // milliseconds ConVar tf_steam_workshop_query_timeout( "tf_steam_workshop_query_timeout", "10", FCVAR_CLIENTDLL, "Time in seconds to allow communication with the Steam Workshop server." ); //----------------------------------------------------------------------------- // Purpose: Utility function //----------------------------------------------------------------------------- static void MakeModalAndBringToFront( vgui::EditablePanel *dialog ) { dialog->SetVisible( true ); if ( dialog->GetParent() == NULL ) { dialog->MakePopup(); } dialog->SetZPos( 10000 ); dialog->MoveToFront(); dialog->SetKeyBoardInputEnabled( true ); dialog->SetMouseInputEnabled( true ); TFModalStack()->PushModal( dialog ); } //----------------------------------------------------------------------------- // Purpose: helper class that contains the list of published files for a user //----------------------------------------------------------------------------- class CPublishedFiles { public: enum { kState_Initialized, kState_PopulatingFileList, kState_ErrorOccurred, kState_DeletingFile, kState_ErrorCannotDeleteFile, kState_Timeout, kState_Done, }; CPublishedFiles(); ~CPublishedFiles(); bool QueryHasTimedOut( void ); void PopulateFileList( void ); bool EnumerateUserPublishedFiles( uint32 unPage ); void RefreshPublishedFileDetails( uint64 nPublishedFileID ); void DeletePublishedFile( uint64 nPublishedFileID ); void ViewPublishedFile( uint64 nPublishedFileID ); const SteamUGCDetails_t *GetPublishedFileDetails( uint64 nPublishedFileID ) const; // Enumerate subscribed files CCallResult m_callbackEnumeratePublishedFiles; void Steam_OnEnumeratePublishedFiles( SteamUGCQueryCompleted_t *pResult, bool bError ); // Callback for deleting files CCallResult m_callbackDeletePublishedFile; void Steam_OnDeletePublishedFile( RemoteStorageDeletePublishedFileResult_t *pResult, bool bError ); unsigned int m_nTotalFilesToQuery; CUtlQueue< PublishedFileId_t > m_FilesToQuery; CUtlMap< PublishedFileId_t, SteamUGCDetails_t > m_FileDetails; long m_nFileQueryTime; bool m_bQueryErrorOccurred; uint32 m_state; uint32 m_unEnumerateStartPage; PublishedFileId_t m_nCurrentQueryPublishedFileID; }; CPublishedFiles::CPublishedFiles() : m_nTotalFilesToQuery( 0 ) , m_nFileQueryTime( 0 ) , m_bQueryErrorOccurred( false ) , m_state( kState_Initialized ) , m_unEnumerateStartPage( 0 ) , m_nCurrentQueryPublishedFileID( 0 ) { m_FileDetails.SetLessFunc( DefLessFunc( PublishedFileId_t ) ); } CPublishedFiles::~CPublishedFiles() { } //----------------------------------------------------------------------------- // Purpose: Determine if our file query has timed out //----------------------------------------------------------------------------- bool CPublishedFiles::QueryHasTimedOut( void ) { return ( ( system()->GetTimeMillis() - m_nFileQueryTime ) > tf_steam_workshop_query_timeout.GetInt() * 1000 ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::PopulateFileList( void ) { // Start the process of showing all of our published files if ( EnumerateUserPublishedFiles( 1 ) ) { m_unEnumerateStartPage = 1; // Get our starting tick m_nFileQueryTime = system()->GetTimeMillis(); m_state = kState_PopulatingFileList; } } //----------------------------------------------------------------------------- // Purpose: Wrapper for creating and sending a CreateQueryUserUGCRequest for this user's published files //----------------------------------------------------------------------------- bool CPublishedFiles::EnumerateUserPublishedFiles( uint32 unPage ) { ISteamUGC *pUGC = steamapicontext->SteamUGC(); ISteamUser *pUser = steamapicontext->SteamUser(); if ( pUGC && pUser ) { AccountID_t nAccountID = pUser->GetSteamID().GetAccountID(); UGCQueryHandle_t ugcHandle = pUGC->CreateQueryUserUGCRequest( nAccountID, k_EUserUGCList_Published, k_EUGCMatchingUGCType_Items, k_EUserUGCListSortOrder_CreationOrderDesc, engine->GetAppID(), engine->GetAppID(), unPage ); // make sure we get the entire description and not a truncated version pUGC->SetReturnLongDescription( ugcHandle, true ); SteamAPICall_t hSteamAPICall = pUGC->SendQueryUGCRequest( ugcHandle ); m_callbackEnumeratePublishedFiles.Set( hSteamAPICall, this, &CPublishedFiles::Steam_OnEnumeratePublishedFiles ); return true; } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::DeletePublishedFile( uint64 nPublishedFileID ) { m_state = kState_DeletingFile; SteamAPICall_t hSteamAPICall = steamapicontext->SteamRemoteStorage()->DeletePublishedFile( nPublishedFileID ); m_callbackDeletePublishedFile.Set( hSteamAPICall, this, &CPublishedFiles::Steam_OnDeletePublishedFile ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const SteamUGCDetails_t *CPublishedFiles::GetPublishedFileDetails( uint64 nPublishedFileID ) const { int idx = m_FileDetails.Find( nPublishedFileID ); if ( idx != m_FileDetails.InvalidIndex() ) { return &m_FileDetails[idx]; } return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::ViewPublishedFile( uint64 nPublishedFileID ) { EUniverse universe = GetUniverse(); switch ( universe ) { case k_EUniversePublic: steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( CFmtStrMax( "http://steamcommunity.com/sharedfiles/filedetails/?id=%llu", nPublishedFileID ) ); break; case k_EUniverseBeta: steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( CFmtStrMax( "http://beta.steamcommunity.com/sharedfiles/filedetails/?id=%llu", nPublishedFileID ) ); break; case k_EUniverseDev: steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( CFmtStrMax( COMMUNITY_DEV_HOST "sharedfiles/filedetails/?id=%llu", nPublishedFileID ) ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::RefreshPublishedFileDetails( uint64 nPublishedFileID ) { if ( m_state != kState_Done ) { // Would confuse the refresh in progress AssertMsg( m_state == kState_Done, "Shouldn't be refreshing file details while other operations are already happening" ); return; } ISteamUGC *pUGC = steamapicontext->SteamUGC(); Assert( pUGC ); if ( pUGC ) { UGCQueryHandle_t ugcHandle = pUGC->CreateQueryUGCDetailsRequest( &nPublishedFileID, 1 ); SteamAPICall_t hSteamAPICall = pUGC->SendQueryUGCRequest( ugcHandle ); m_callbackEnumeratePublishedFiles.Set( hSteamAPICall, this, &CPublishedFiles::Steam_OnEnumeratePublishedFiles ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::Steam_OnDeletePublishedFile( RemoteStorageDeletePublishedFileResult_t *pResult, bool bError ) { if ( pResult && !bError ) { if ( pResult->m_eResult != k_EResultOK ) { m_state = kState_ErrorOccurred; if ( pResult->m_eResult == k_EResultAccessDenied ) { m_state = kState_ErrorCannotDeleteFile; } } else { m_state = kState_Done; m_FileDetails.Remove( pResult->m_nPublishedFileId ); } } else { m_state = kState_ErrorOccurred; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPublishedFiles::Steam_OnEnumeratePublishedFiles( SteamUGCQueryCompleted_t *pResult, bool bError ) { // Make sure we succeeded if ( bError || pResult->m_eResult != k_EResultOK ) { m_state = kState_ErrorOccurred; return; } if ( m_state == kState_PopulatingFileList && m_unEnumerateStartPage == 1 ) { // Start from scratch m_FilesToQuery.Purge(); m_FileDetails.Purge(); } const int nNumFiles = pResult->m_unNumResultsReturned; for ( int i = 0; i < nNumFiles; i++ ) { SteamUGCDetails_t sDetails = { 0 }; steamapicontext->SteamUGC()->GetQueryUGCResult( pResult->m_handle, i, &sDetails ); m_FileDetails.InsertOrReplace( sDetails.m_nPublishedFileId, sDetails ); } if ( m_state == kState_Done ) { // This was a one-off request from e.g. RefreshPublishedFileDetails return; } if ( !nNumFiles || m_FileDetails.Count() >= pResult->m_unTotalMatchingResults ) { m_state = kState_Done; return; } EnumerateUserPublishedFiles( ++m_unEnumerateStartPage ); } // Tags static const char *kClassTags[TF_LAST_NORMAL_CLASS] = { "", // TF_CLASS_UNDEFINED "Scout", // TF_CLASS_SCOUT "Sniper", // TF_CLASS_SNIPER, "Soldier", // TF_CLASS_SOLDIER, "Demoman", // TF_CLASS_DEMOMAN, "Medic", // TF_CLASS_MEDIC, "Heavy", //TF_CLASS_HEAVYWEAPONS, "Pyro", // TF_CLASS_PYRO, "Spy", // TF_CLASS_SPY, "Engineer", // TF_CLASS_ENGINEER, }; struct TagPair_t { const char *pCheckboxElementName; const char *pTag; }; static TagPair_t kOtherTags[] = { { "TagCheckbox_Headgear", "Headgear" }, { "TagCheckbox_Weapon", "Weapon" }, { "TagCheckbox_Misc", "Misc" }, { "TagCheckbox_SoundDevice", "Sound Device" }, { "TagCheckbox_Halloween", "Halloween" }, { "TagCheckbox_Taunt", "Taunt" }, { "TagCheckbox_UnusualEffect", "Unusual Effect" }, { "TagCheckbox_Jungle", "Jungle" }, }; static uint32 kNumOtherTags = ARRAYSIZE( kOtherTags ); // Map Tags static TagPair_t kMapTags[] = { { "MapsCheckBox_CTF", "Capture the Flag" }, { "MapsCheckBox_CP", "Control Point" }, { "MapsCheckBox_Escort", "Payload" }, { "MapsCheckBox_EscortRace", "Payload Race" }, { "MapsCheckBox_Arena", "Arena" }, { "MapsCheckBox_Koth", "King of the Hill" }, { "MapsCheckBox_AttackDefense", "Attack / Defense" }, { "MapsCheckBox_SD", "Special Delivery" }, { "MapsCheckBox_RobotDestruction", "Robot Destruction" }, { "MapsCheckBox_MVM", "Mann vs. Machine" }, { "MapsCheckBox_Powerup", "Mannpower" }, { "MapsCheckBox_Medieval", "Medieval" }, { "MapsCheckBox_PassTime", "PASS Time" }, { "MapsCheckBox_Specialty", "Specialty" }, { "MapsCheckBox_Halloween", "Halloween" }, { "MapsCheckbox_Smissmas", "Smissmas" }, { "MapsCheckbox_Night", "Night" }, { "MapsCheckbox_Jungle", "Jungle" }, }; static uint32 kNumMapTags = ARRAYSIZE( kMapTags ); static const char *kImportedTag = "Certified Compatible"; //----------------------------------------------------------------------------- // Purpose: Publish file dialog //----------------------------------------------------------------------------- class CTFFilePublishDialog : public CFilePublishDialog { DECLARE_CLASS_SIMPLE( CTFFilePublishDialog, CFilePublishDialog ); public: CTFFilePublishDialog( Panel *parent, const char *name, PublishedFileDetails_t *pDetails ) : CFilePublishDialog( parent, name, pDetails ), m_bImported(false) {} virtual ErrorCode_t ValidateFile( const char *lpszFilename ) { if( !g_pFullFileSystem->FileExists( lpszFilename ) ) return kFailedFileNotFound; // TODO This is the nominal max of SteamUGC, but should be found dynamically const uint32 kMaxFileSize = 400 * 1024 * 1024; unsigned int unFileSize = g_pFullFileSystem->Size( lpszFilename ); if ( unFileSize == 0 || unFileSize > kMaxFileSize ) { return kFailedFileTooLarge; } return kNoError; } virtual AppId_t GetTargetAppID( void ) { return engine->GetAppID(); } virtual unsigned int DesiredPreviewHeight( void ) { return TF2_PREVIEW_IMAGE_HEIGHT; } virtual unsigned int DesiredPreviewWidth( void ) { return TF2_PREVIEW_IMAGE_WIDTH; } virtual bool BForceSquarePreviewImage( void ) { return true; } virtual EWorkshopFileType WorkshipFileTypeForFile( const char *pszFileName ) { const char *pExt = V_GetFileExtension( pszFileName ); if ( pExt && V_strcmp( pExt, "bsp" ) == 0 ) { return k_EWorkshopFileTypeCommunity; } AssertMsg( pExt && V_strcmp( pExt, "zip" ) == 0, "Unrecognized file type, defaulting to microtransaction\n" ); return k_EWorkshopFileTypeMicrotransaction; } virtual const char *GetPreviewFileTypes( void ) { return "*.tga,*.jpg,*.png"; } virtual const char *GetPreviewFileTypeDescriptions( void ) { return "#TF_SteamWorkshop_Images"; } virtual const char *GetFileTypes( eFilterType_t eType = IMPORT_FILTER_NONE ) OVERRIDE { if ( eType == IMPORT_FILTER_MAP ) { return "*.bsp"; } return "*.zip"; } virtual const char *GetFileTypeDescriptions( eFilterType_t eType = IMPORT_FILTER_NONE ) OVERRIDE { if ( eType == IMPORT_FILTER_MAP ) { return "#TF_SteamWorkshop_AcceptableFilesMaps"; } return "#TF_SteamWorkshop_AcceptableFiles"; } virtual const char *GetResFile() const { return "Resource/UI/PublishFileDialog.res"; } virtual void SetFile( const char *lpszFilename, bool bImported ) { BaseClass::SetFile( lpszFilename, bImported ); SetTagsVisible( true, WorkshipFileTypeForFile( lpszFilename ) ); m_sFilePath = lpszFilename; CUtlBuffer buffer; g_pFullFileSystem->ReadFile( lpszFilename, NULL, buffer ); m_fileCRC = CRC32_ProcessSingleBuffer( buffer.Base(), buffer.Size() ); m_bImported = bImported; } void SetTagsVisible( bool visible, EWorkshopFileType eFileType = k_EWorkshopFileTypeCommunity ) { uint32 i; vgui::CheckButton *pButton; vgui::Label *pLabel = FindControl( "TagsTitle", true ); if ( pLabel ) { pLabel->SetVisible( visible ); } if ( !visible ) { // no tags visible, so hide everything // class tags for ( i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; i++ ) { pButton = FindControl( VarArgs( "ClassCheckBox%d", i ), true ); if ( pButton ) { pButton->SetVisible( visible ); } } // other tags for ( i = 0; i < kNumOtherTags; ++i ) { pButton = FindControl( kOtherTags[i].pCheckboxElementName, true ); if ( pButton ) { pButton->SetVisible( visible ); } } // map tags for ( i = 0; i < kNumMapTags; ++i ) { pButton = FindControl( kMapTags[i].pCheckboxElementName, true ); if ( pButton ) { pButton->SetVisible( visible ); } } } else { bool bIsMap = ( eFileType == k_EWorkshopFileTypeCommunity ); // class tags for ( i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; i++ ) { pButton = FindControl( VarArgs( "ClassCheckBox%d", i ), true ); if ( pButton ) { pButton->SetVisible( !bIsMap ); if ( bIsMap && pButton->IsSelected() ) { pButton->SetSelected( false ); // reset this button if we're using the maps tags } } } // other tags for ( i = 0; i < kNumOtherTags; ++i ) { pButton = FindControl( kOtherTags[i].pCheckboxElementName, true ); if ( pButton ) { pButton->SetVisible( !bIsMap ); if ( bIsMap && pButton->IsSelected() ) { pButton->SetSelected( false ); // reset this button if we're using the maps tags } } } // map tags for ( i = 0; i < kNumMapTags; ++i ) { pButton = FindControl( kMapTags[i].pCheckboxElementName, true ); if ( pButton ) { pButton->SetVisible( bIsMap ); if ( !bIsMap && pButton->IsSelected() ) { pButton->SetSelected( false ); // reset this button if we're not using the maps tags } } } } } virtual void PopulateTags( SteamParamStringArray_t &strArray ) { m_vecTags.RemoveAll(); // class tags vgui::EditablePanel* pClassUsagePanel = dynamic_cast( FindChildByName( "ClassUsagePanel" ) ); if ( pClassUsagePanel ) { for ( int i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; i++ ) { if ( IsChildButtonSelected( pClassUsagePanel, VarArgs("ClassCheckBox%d",i), false ) ) { m_vecTags.AddToTail( kClassTags[i] ); } } } // other tags for ( uint32 i = 0; i < kNumOtherTags; ++i ) { if ( IsChildButtonSelected( this, kOtherTags[i].pCheckboxElementName, true ) ) { m_vecTags.AddToTail( kOtherTags[i].pTag ); } } // map tags for ( uint32 i = 0; i < kNumMapTags; ++i ) { if ( IsChildButtonSelected( this, kMapTags[i].pCheckboxElementName, true ) ) { m_vecTags.AddToTail( kMapTags[i].pTag ); } } if ( m_bImported ) { m_vecTags.AddToTail( kImportedTag ); } strArray.m_ppStrings = m_vecTags.Base(); strArray.m_nNumStrings = m_vecTags.Count(); } virtual void PerformLayout() { BaseClass::PerformLayout(); // Center it, keeping requested size int x, y, ww, wt, wide, tall; vgui::surface()->GetWorkspaceBounds( x, y, ww, wt ); GetSize(wide, tall); SetPos(x + ((ww - wide) / 2), y + ((wt - tall) / 2)); } virtual bool PrepSteamCloudFilePath( const char *lpszFileName, CUtlString &steamCloudFileName ) { char szShortName[ MAX_PATH ]; Q_FileBase( lpszFileName, szShortName, sizeof( szShortName ) ); const char *szExt = Q_GetFileExtension( lpszFileName ); Q_SetExtension( szShortName, CFmtStr( ".%s", szExt ).Access(), sizeof(szShortName ) ); steamCloudFileName.Format( "steamworkshop/tf2/%s", szShortName ); return true; } virtual void ErrorMessage( ErrorCode_t errorCode, KeyValues *pkvTokens ) { switch ( errorCode ) { case kFailedToPublishFile: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedToPublishFile", "#GameUI_OK" ); break; case kFailedToPrepareFile: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedToPrepareFile", "#GameUI_OK" ); break; case kFailedToUpdateFile: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedToUpdateFile", "#GameUI_OK" ); break; case kSteamCloudNotAvailable: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kSteamCloudNotAvailable", "#GameUI_OK" ); break; case kSteamExceededCloudQuota: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kSteamExceededCloudQuota", "#GameUI_OK" ); break; case kFailedToWriteToSteamCloud: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedToWriteToSteamCloud", "#GameUI_OK" ); break; case kFileNotFound: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFileNotFound", "#GameUI_OK" ); break; case kNeedTitleAndDescription: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kNeedTitleAndDescription", "#GameUI_OK" ); break; case kFailedFileValidation: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedFileValidation", "#GameUI_OK" ); break; case kFailedFileTooLarge: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedFileTooLarge", "#GameUI_OK" ); break; case kFailedFileNotFound: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedFileNotFound", "#GameUI_OK" ); break; case kFailedUserModifiedFile: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kFailedUserModifiedFile", "#GameUI_OK" ); break; case kInvalidMapName: ShowMessageBox( "#TF_PublishFile_Error", "#TF_PublishFile_kInvalidMapName", "#GameUI_OK" ); break; default: Assert( false ); // Unhandled enum value break; } if ( pkvTokens ) { pkvTokens->deleteThis(); } } void ApplySchemeSettings( vgui::IScheme *pScheme ) { BaseClass::ApplySchemeSettings( pScheme ); if ( !m_bAddingNewFile ) { bool bIsMap = ( m_FileDetails.publishedFileDetails.m_eFileType == k_EWorkshopFileTypeCommunity ); CExImageButton *pImageButton = FindControl( "ButtonSourceCosmetics", true ); if ( pImageButton ) { pImageButton->SetVisible( !bIsMap ); } vgui::Button *pButton = FindControl