//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "cbase.h" #include "econ_item_inventory.h" #include "vgui/ILocalize.h" #include "tier3/tier3.h" #include "econ_item_system.h" #include "econ_item.h" #include "econ_gcmessages.h" #include "shareddefs.h" #include "filesystem.h" #include "econ_item_description.h" // only for CSteamAccountIDAttributeCollector #ifdef CLIENT_DLL #include #include "econ_game_account_client.h" #include "ienginevgui.h" #include "econ_ui.h" #include "item_pickup_panel.h" #include "econ/econ_item_preset.h" #include "econ/confirm_dialog.h" #include "tf_xp_source.h" #include "tf_notification.h" #else #include "props_shared.h" #include "basemultiplayerplayer.h" #endif #if defined(TF_CLIENT_DLL) || defined(TF_DLL) #include "tf_gcmessages.h" #include "tf_duel_summary.h" #include "econ_contribution.h" #include "tf_player_info.h" #include "econ/econ_claimcode.h" #include "tf_wardata.h" #include "tf_ladder_data.h" #include "tf_rating_data.h" #endif #if defined(TF_DLL) && defined(GAME_DLL) #include "tf_gc_api.h" #include "econ/econ_game_account_server.h" #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" using namespace GCSDK; #ifdef _DEBUG ConVar item_inventory_debug( "item_inventory_debug", "0", FCVAR_REPLICATED | FCVAR_CHEAT ); #endif #ifdef USE_DYNAMIC_ASSET_LOADING //extern ConVar item_dynamicload; #endif #define ITEM_CLIENTACK_FILE "item_clientacks.txt" #ifdef _DEBUG #ifdef CLIENT_DLL ConVar item_debug_clientacks( "item_debug_clientacks", "0", FCVAR_CLIENTDLL | FCVAR_ARCHIVE ); #endif #endif // _DEBUG // Result codes strings for GC results. const char* GCResultString[8] = { "k_EGCMsgResponseOK", // Request succeeded "k_EGCMsgResponseDenied", // Request denied "k_EGCMsgResponseServerError", // Request failed due to a temporary server error "k_EGCMsgResponseTimeout", // Request timed out "k_EGCMsgResponseInvalid", // Request was corrupt "k_EGCMsgResponseNoMatch", // No item definition matched the request "k_EGCMsgResponseUnknownError", // Request failed with an unknown error "k_EGCMsgResponseNotLoggedOn", // Client not logged on to steam }; CBasePlayer *GetPlayerBySteamID( const CSteamID &steamID ) { CSteamID steamIDPlayer; for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); if ( pPlayer == NULL ) continue; if ( pPlayer->GetSteamID( &steamIDPlayer ) == false ) continue; if ( steamIDPlayer == steamID ) return pPlayer; } return NULL; } // Inventory Less function. // Used to sort the inventory items into their positions. bool CInventoryListLess::Less( const CEconItemView &src1, const CEconItemView &src2, void *pCtx ) { int iPos1 = src1.GetInventoryPosition(); int iPos2 = src2.GetInventoryPosition(); // Context can be specified to point to a func that extracts the position from the backend position. // Necessary if your inventory packs a bunch of info into the position instead of using it just as a position. if ( pCtx ) { CPlayerInventory *pInv = (CPlayerInventory*)pCtx; iPos1 = pInv->ExtractInventorySortPosition( iPos1 ); iPos2 = pInv->ExtractInventorySortPosition( iPos2 ); } if ( iPos1 < iPos2 ) return true; return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CInventoryManager::CInventoryManager( void ) #ifdef CLIENT_DLL : m_mapPersonaNamesCache( DefLessFunc( uint32 ) ) , m_sPersonaStateChangedCallback( this, &CInventoryManager::OnPersonaStateChanged ) , m_personaNameRequests( DefLessFunc( uint64 ) ) #endif { #ifdef CLIENT_DLL m_pkvItemClientAckFile = NULL; m_bClientAckDirty = false; m_iPredictedDiscards = 0; m_flNextLoadPresetChange = 0.0f; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SteamRequestInventory( CPlayerInventory *pInventory, CSteamID pSteamID, IInventoryUpdateListener *pListener ) { // SteamID must be valid if ( !pSteamID.IsValid() || !pSteamID.BIndividualAccount() ) { if ( !HushAsserts() ) { Assert( pSteamID.IsValid() ); Assert( pSteamID.BIndividualAccount() ); } return; } // If we haven't seen this inventory before, register it bool bFound = false; for ( int i = 0; i < m_pInventories.Count(); i++ ) { if ( m_pInventories[i].pInventory == pInventory ) { bFound = true; break; } } if ( !bFound ) { int iIdx = m_pInventories.AddToTail(); m_pInventories[iIdx].pInventory = pInventory; m_pInventories[iIdx].pListener = pListener; } // Add the request to our list of pending requests int iIdx = m_hPendingInventoryRequests.AddToTail(); m_hPendingInventoryRequests[iIdx].pID = pSteamID; m_hPendingInventoryRequests[iIdx].pInventory = pInventory; pInventory->RequestInventory( pSteamID ); if( pListener ) { pInventory->AddListener( pListener ); } } //----------------------------------------------------------------------------- // Purpose: Called when a gameserver connects to steam. //----------------------------------------------------------------------------- void CInventoryManager::GameServerSteamAPIActivated() { #if defined(TF_DLL) && defined(GAME_DLL) GameCoordinator_NotifyGameState(); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CPlayerInventory *CInventoryManager::GetInventoryForAccount( uint32 iAccountID ) { FOR_EACH_VEC( m_pInventories, i ) { if ( m_pInventories[i].pInventory->GetOwner().GetAccountID() == iAccountID ) return m_pInventories[i].pInventory; } return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::DeregisterInventory( CPlayerInventory *pInventory ) { int iCount = m_pInventories.Count(); for ( int i = iCount-1; i >= 0; i-- ) { if ( m_pInventories[i].pInventory == pInventory ) { m_pInventories.Remove(i); } } } #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::IsPresetIndexValid( equipped_preset_t unPreset ) { const bool bResult = GetItemSchema()->IsValidPreset( unPreset ); AssertMsg( bResult, "Invalid preset index!" ); return bResult; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::LoadPreset( equipped_class_t unClass, equipped_preset_t unPreset ) { if ( !IsValidPlayerClass( unClass ) ) return false; if ( !IsPresetIndexValid( unPreset ) ) return false; if ( !GetLocalInventory()->GetSOC() ) return false; if ( m_flNextLoadPresetChange > gpGlobals->realtime ) { Msg( "Loadout change denied. Changing presets too quickly.\n" ); return false; } m_flNextLoadPresetChange = gpGlobals->realtime + 0.5f; GCSDK::CProtoBufMsg msg( k_EMsgGCPresets_SelectPresetForClass ); msg.Body().set_class_id( unClass ); msg.Body().set_preset_id( unPreset ); GCClientSystem()->BSendMessage( msg ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::UpdateLocalInventory( void ) { if ( steamapicontext->SteamUser() && GetLocalInventory() ) { CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); if ( steamID.IsValid() ) // make sure we're logged in and we know who we are { SteamRequestInventory( GetLocalInventory(), steamID ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::OnPersonaStateChanged( PersonaStateChange_t *info ) { if ( ( info->m_nChangeFlags & k_EPersonaChangeName ) != 0 ) m_personaNameRequests.InsertOrReplace( info->m_ulSteamID, true ); } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::Init( void ) { return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::PostInit( void ) { // Initialize the item system. ItemSystem()->Init(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::PreInitGC() { REG_SHARED_OBJECT_SUBCLASS( CEconItem ); #if defined (CLIENT_DLL) REG_SHARED_OBJECT_SUBCLASS( CEconGameAccountClient ); REG_SHARED_OBJECT_SUBCLASS( CEconItemPerClassPresetData ); REG_SHARED_OBJECT_SUBCLASS( CSOTFMatchResultPlayerInfo ); REG_SHARED_OBJECT_SUBCLASS( CXPSource ); REG_SHARED_OBJECT_SUBCLASS( CTFNotification ); #endif #if defined(TF_CLIENT_DLL) || defined(TF_DLL) REG_SHARED_OBJECT_SUBCLASS( CWarData ); REG_SHARED_OBJECT_SUBCLASS( CTFDuelSummary ); REG_SHARED_OBJECT_SUBCLASS( CTFMapContribution ); REG_SHARED_OBJECT_SUBCLASS( CTFPlayerInfo ); REG_SHARED_OBJECT_SUBCLASS( CEconClaimCode ); REG_SHARED_OBJECT_SUBCLASS( CSOTFLadderData ); #endif #ifdef TF_DLL REG_SHARED_OBJECT_SUBCLASS( CEconGameAccountForGameServers ); #endif // TF_DLL } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::PostInitGC() { #ifdef CLIENT_DLL // The client immediately loads the local player's inventory UpdateLocalInventory(); #endif } //----------------------------------------------------------------------------- void CInventoryManager::Shutdown() { int nInventoryCount = m_pInventories.Count(); for ( int iInventory = 0; iInventory < nInventoryCount; ++iInventory ) { CPlayerInventory *pInventory = m_pInventories[iInventory].pInventory; if ( pInventory ) { pInventory->Clear(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::LevelInitPreEntity( void ) { // Throw out any testitem definitions for ( int i = 0; i < TI_TYPE_COUNT; i++ ) { int iNewDef = TESTITEM_DEFINITIONS_BEGIN_AT + i; ItemSystem()->GetItemSchema()->ItemTesting_DiscardTestDefinition( iNewDef ); } // Precache all item models we've got #ifdef GAME_DLL CUtlVector vecPrecacheModelStrings; #endif // GAME_DLL const CEconItemSchema::ItemDefinitionMap_t& mapItemDefs = ItemSystem()->GetItemSchema()->GetItemDefinitionMap(); FOR_EACH_MAP_FAST( mapItemDefs, i ) { CEconItemDefinition *pData = mapItemDefs[i]; pData->SetHasBeenLoaded( true ); #ifdef GAME_DLL bool bDynamicLoad = false; #ifdef USE_DYNAMIC_ASSET_LOADING bDynamicLoad = true;//item_dynamicload.GetBool(); #endif // USE_DYNAMIC_ASSET_LOADING pData->GeneratePrecacheModelStrings( bDynamicLoad, &vecPrecacheModelStrings ); // Precache the models and the gibs for everything the definition requested. FOR_EACH_VEC( vecPrecacheModelStrings, i ) { // Ignore any objects which requested an empty precache string for whatever reason. if ( vecPrecacheModelStrings[i] && vecPrecacheModelStrings[i][0] ) { int iModelIndex = CBaseEntity::PrecacheModel( vecPrecacheModelStrings[i] ); PrecacheGibsForModel( iModelIndex ); } } vecPrecacheModelStrings.RemoveAll(); pData->GeneratePrecacheSoundStrings( bDynamicLoad, &vecPrecacheModelStrings ); // Precache the sounds for everything FOR_EACH_VEC( vecPrecacheModelStrings, i ) { // Ignore any objects which requested an empty precache string for whatever reason. if ( vecPrecacheModelStrings[i] && vecPrecacheModelStrings[i][0] ) { CBaseEntity::PrecacheScriptSound( vecPrecacheModelStrings[i] ); } } vecPrecacheModelStrings.RemoveAll(); #endif } // We reset the cached attribute class strings, since it's invalidated by level changes ItemSystem()->ResetAttribStringCache(); #ifdef GAME_DLL ItemSystem()->ReloadWhitelist(); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::LevelShutdownPostEntity( void ) { // We reset the cached attribute class strings, since it's invalidated by level changes ItemSystem()->ResetAttribStringCache(); } //----------------------------------------------------------------------------- // Purpose: Lets the client know that we're now connected to the GC //----------------------------------------------------------------------------- #ifdef CLIENT_DLL void CInventoryManager::SendGCConnectedEvent( void ) { IGameEvent *event = gameeventmanager->CreateEvent( "gc_connected" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } } #endif #if !defined(NO_STEAM) //----------------------------------------------------------------------------- // Purpose: GC Msg handler to receive the dev "new item" response //----------------------------------------------------------------------------- class CGCDev_NewItemRequestResponse : public GCSDK::CGCClientJob { public: CGCDev_NewItemRequestResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { GCSDK::CGCMsg msg( pNetPacket ); if ( msg.Body().m_eResponse == k_EGCMsgResponseOK ) { Msg("Received new item acknowledgement: %s\n", GCResultString[msg.Body().m_eResponse] ); } else { Warning("Failed to generate new item: %s\n", GCResultString[msg.Body().m_eResponse] ); } return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGCDev_NewItemRequestResponse, "CGCDev_NewItemRequestResponse", k_EMsgGCDev_NewItemRequestResponse, GCSDK::k_EServerTypeGCClient ); #endif // NO_STEAM //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::RemovePendingRequest( CSteamID *pSteamID ) { #ifdef CLIENT_DLL // Only the client, all requests are for the local player. Clear them all. m_hPendingInventoryRequests.Purge(); return; #endif // On the server, remove all requests for the specified steam id int iCount = m_hPendingInventoryRequests.Count(); for ( int i = iCount-1; i >= 0; i-- ) { if ( m_hPendingInventoryRequests[i].pID == *pSteamID ) { m_hPendingInventoryRequests.Remove(i); } } } #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::DropItem( itemid_t iItemID ) { static CSchemaAttributeDefHandle pAttrDef_NoDelete( "cannot delete" ); // Double check that this item can be delete CEconItemView *pItem = GetLocalInventory()->GetInventoryItemByItemID( iItemID ); if ( !pItem || !pAttrDef_NoDelete || pItem->FindAttribute( pAttrDef_NoDelete ) ) { return; } GCSDK::CGCMsg msg( k_EMsgGCDelete ); msg.Body().m_unItemID = iItemID; GCClientSystem()->BSendMessage( msg ); // Keep track of how many items we've discarded, but haven't received responses for. m_iPredictedDiscards++; } //----------------------------------------------------------------------------- // Purpose: Delete any items we can't find static data for. This can happen when we're testing // internally, and then remove an item. Shouldn't ever happen in the wild. //----------------------------------------------------------------------------- int CInventoryManager::DeleteUnknowns( CPlayerInventory *pInventory ) { // We need to manually walk the main inventory's SOC, because unknown items won't be in the inventory GCSDK::CGCClientSharedObjectCache *pSOC = pInventory->GetSOC(); if ( pSOC ) { int iBadItems = 0; CGCClientSharedObjectTypeCache *pTypeCache = pSOC->FindTypeCache( CEconItem::k_nTypeID ); if( pTypeCache ) { for( uint32 unItem = 0; unItem < pTypeCache->GetCount(); unItem++ ) { CEconItem *pItem = (CEconItem *)pTypeCache->GetObject( unItem ); if ( pItem ) { CEconItemDefinition *pData = ItemSystem()->GetStaticDataForItemByDefIndex( pItem->GetDefinitionIndex() ); if ( !pData ) { DropItem( pItem->GetItemID() ); iBadItems++; } } } } return iBadItems; } return 0; } //----------------------------------------------------------------------------- // Purpose: Tries to move the specified item into the player's backpack. // FAILS if the backpack is full. Returns false in that case. //----------------------------------------------------------------------------- bool CInventoryManager::SetItemBackpackPosition( CEconItemView *pItem, uint32 iPosition, bool bForceUnequip, bool bAllowOverflow ) { CPlayerInventory *pInventory = GetLocalInventory(); if ( !pInventory ) return false; const int iMaxItems = pInventory->GetMaxItemCount(); if ( !iPosition ) { // Build a list of empty slots. We track extra slots beyond the backpack for overflow. CUtlVector< bool > bFilledSlots; bFilledSlots.SetSize( iMaxItems * 2 ); for ( int i = 0; i < bFilledSlots.Count(); ++i ) { bFilledSlots[i] = false; } for ( int i = 0; i < pInventory->GetItemCount(); i++ ) { CEconItemView *pTmpItem = pInventory->GetItem(i); // Ignore the item we're moving. if ( pTmpItem == pItem ) continue; int iBackpackPos = GetBackpackPositionFromBackend( pTmpItem->GetInventoryPosition() ); if ( iBackpackPos >= 0 && iBackpackPos < bFilledSlots.Count() ) { bFilledSlots[iBackpackPos] = true; } } // Add predicted filled slots for ( int i = 0; i < m_PredictedFilledSlots.Count(); i++ ) { int iBackpackPos = m_PredictedFilledSlots[i]; if ( iBackpackPos >= 0 && iBackpackPos < bFilledSlots.Count() ) { bFilledSlots[iBackpackPos] = true; } } // Now find an empty slot for ( int i = 1; i < bFilledSlots.Count(); i++ ) { if ( !bFilledSlots[i] ) { iPosition = i; break; } } if ( !iPosition ) return false; } if ( !bAllowOverflow && iPosition > (uint32)iMaxItems ) return false; //Warning("Moved item %llu to backpack slot: %d\n", pItem->GetItemID(), iPosition ); uint32 iBackendPosition = bForceUnequip ? 0 : pItem->GetInventoryPosition(); SetBackpackPosition( &iBackendPosition, iPosition ); UpdateInventoryPosition( pInventory, pItem->GetItemID(), iBackendPosition ); m_PredictedFilledSlots.AddToTail( iPosition ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::MoveItemToBackpackPosition( CEconItemView *pItem, int iBackpackPosition ) { CEconItemView *pOldItem = GetItemByBackpackPosition( iBackpackPosition ); if ( pOldItem ) { // Move the item in the new spot to our current spot SetItemBackpackPosition( pOldItem, GetBackpackPositionFromBackend(pItem->GetInventoryPosition()) ); //Warning("Moved OLD item %llu to backpack slot: %d\n", pOldItem->GetItemID(), GetBackpackPositionFromBackend(iBackendPosition) ); } // Move the item to the new spot SetItemBackpackPosition( pItem, iBackpackPosition ); //Warning("Moved item %llu to backpack slot: %d\n", pItem->GetItemID(), iBackpackPosition ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- class CWaitForBackpackSortFinishDialog : public CGenericWaitingDialog { public: CWaitForBackpackSortFinishDialog( vgui::Panel *pParent ) : CGenericWaitingDialog( pParent ) { } protected: virtual void OnTimeout() { InventoryManager()->SortBackpackFinished(); } }; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SortBackpackBy( uint32 iSortType ) { GCSDK::CProtoBufMsg msg( k_EMsgGCSortItems ); msg.Body().set_sort_type( iSortType ); GCClientSystem()->BSendMessage( msg ); ShowWaitingDialog( new CWaitForBackpackSortFinishDialog( NULL ), "#BackpackSortExplanation_Title", true, false, 3.0f ); m_bInBackpackSort = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SortBackpackFinished( void ) { m_bInBackpackSort = false; GetLocalInventory()->SendInventoryUpdateEvent(); } //----------------------------------------------------------------------------- // Purpose: GC Msg handler to receive the sort finished message //----------------------------------------------------------------------------- class CGBackpackSortFinished : public GCSDK::CGCClientJob { public: CGBackpackSortFinished( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { CloseWaitingDialog(); InventoryManager()->SortBackpackFinished(); return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGBackpackSortFinished, "CGBackpackSortFinished", k_EMsgGCBackpackSortFinished, GCSDK::k_EServerTypeGCClient ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::UpdateInventoryPosition( CPlayerInventory *pInventory, uint64 ulItemID, uint32 unNewInventoryPos ) { if ( !pInventory->GetInventoryItemByItemID( ulItemID ) ) { Warning("Attempt to update inventory position failure: %s.\n", "could not find matching item ID"); return; } if ( !pInventory->GetSOCDataForItem( ulItemID ) ) { Warning("Attempt to update inventory position failure: %s\n", "could not find SOC data for item"); return; } // In the incredibly rare case where the GC crashed while sorting our backpack, we won't have gotten // a k_EMsgGCBackpackSortFinished message. Assume that if we're requesting a manual move of an item, we're not sorting anymore. m_bInBackpackSort = false; // TF has multiple ways of using the inventory position bits. For all inventory positions moving forward, assume // they're in the new format. #if defined(TF_CLIENT_DLL) || defined(TF_DLL) if ( unNewInventoryPos != 0 ) { unNewInventoryPos |= kBackendPosition_NewFormat; } #endif // defined(TF_CLIENT_DLL) || defined(TF_DLL) // Queue a message to be sent to the GC CMsgSetItemPositions_ItemPosition *pMsg = m_msgPendingSetItemPositions.add_item_positions(); pMsg->set_item_id( ulItemID ); pMsg->set_position( unNewInventoryPos ); } void CInventoryManager::Update( float frametime ) { // Check if we have any pending item position changes that we need to flush out if ( m_msgPendingSetItemPositions.item_positions_size() > 0 ) { // !KLUDGE! It would be nice if we could just send this in one line instead of making a copy CProtoBufMsg msg( k_EMsgGCSetItemPositions ); msg.Body() = m_msgPendingSetItemPositions; GCClientSystem()->BSendMessage( msg ); m_msgPendingSetItemPositions.Clear(); } // Check if we have any pending account lookups to batch up if ( m_msgPendingLookupAccountNames.accountids_size() > 0 ) { // !KLUDGE! It would be nice if we could just send this in one line instead of making a copy CProtoBufMsg< CMsgLookupMultipleAccountNames > msg( k_EMsgGCLookupMultipleAccountNames ); msg.Body() = m_msgPendingLookupAccountNames; GCClientSystem()->BSendMessage( msg ); m_msgPendingLookupAccountNames.Clear(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::UpdateInventoryEquippedState( CPlayerInventory *pInventory, uint64 ulItemID, equipped_class_t unClass, equipped_slot_t unSlot ) { // passing in INVALID_ITEM_ID means "unequip from this slot" if ( ulItemID != INVALID_ITEM_ID ) { if ( !pInventory->GetInventoryItemByItemID( ulItemID ) ) { //Warning("Attempt to update equipped state failure: %s.\n", "could not find matching item ID"); return; } if ( !pInventory->GetSOCDataForItem( ulItemID ) ) { //Warning("Attempt to update equipped state failure: %s\n", "could not find SOC data for item"); return; } } CProtoBufMsg msg( k_EMsgGCAdjustItemEquippedState ); msg.Body().set_item_id( ulItemID ); msg.Body().set_new_class( unClass ); msg.Body().set_new_slot( unSlot ); GCClientSystem()->BSendMessage( msg ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::ShowItemsPickedUp( bool bForce, bool bReturnToGame, bool bNoPanel ) { CPlayerInventory *pLocalInv = GetLocalInventory(); if ( !pLocalInv ) return false; // Don't bring it up if we're already browsing something in the gameUI vgui::VPANEL gameuiPanel = enginevgui->GetPanel( PANEL_GAMEUIDLL ); if ( !bForce && vgui::ipanel()->IsVisible( gameuiPanel ) ) return false; CUtlVector aItemsFound; // Go through the root inventory and find any items that are in the "found" position int iCount = pLocalInv->GetItemCount(); for ( int i = 0; i < iCount; i++ ) { CEconItemView *pTmp = pLocalInv->GetItem(i); if ( !pTmp ) continue; if ( pTmp->GetStaticData()->IsHidden() ) continue; uint32 iPosition = pTmp->GetInventoryPosition(); if ( IsUnacknowledged(iPosition) == false ) continue; if ( GetBackpackPositionFromBackend(iPosition) != 0 ) continue; // Now make sure we haven't got a clientside saved ack for this item. // This makes sure we don't show multiple pickups for items that we've found, // but haven't been able to move out of unack'd position due to the GC being unavailable. if ( HasBeenAckedByClient( pTmp ) ) continue; aItemsFound.AddToTail( pTmp ); } if ( !aItemsFound.Count() ) return CheckForRoomAndForceDiscard(); // We're not forcing the player to make room yet. Just show the pickup panel. CItemPickupPanel *pItemPanel = bNoPanel ? NULL : EconUI()->OpenItemPickupPanel(); if ( pItemPanel ) { pItemPanel->SetReturnToGame( bReturnToGame ); } for ( int i = 0; i < aItemsFound.Count(); i++ ) { if ( pItemPanel ) { pItemPanel->AddItem( aItemsFound[i] ); } else { AcknowledgeItem( aItemsFound[i] ); } } if ( pItemPanel ) { pItemPanel->MoveToFront(); } else { SaveAckFile(); } aItemsFound.Purge(); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::CheckForRoomAndForceDiscard( void ) { CPlayerInventory *pLocalInv = GetLocalInventory(); if ( !pLocalInv ) return false; // Go through the inventory and attempt to move any items outside the backpack into valid positions. // Remember the first item that we failed to move, so we can force a discard later. CEconItemView *pItem = NULL; const int iMaxItems = pLocalInv->GetMaxItemCount(); int iCount = pLocalInv->GetItemCount(); for ( int i = 0; i < iCount; i++ ) { CEconItemView *pTmp = pLocalInv->GetItem(i); if ( !pTmp ) continue; if ( pTmp->GetStaticData()->IsHidden() ) continue; uint32 iPosition = pTmp->GetInventoryPosition(); if ( IsUnacknowledged(iPosition) || GetBackpackPositionFromBackend(iPosition) > iMaxItems ) { if ( !SetItemBackpackPosition( pTmp, 0, false, false ) ) { pItem = pTmp; break; } } } // If we're not over the limit, we're done. if ( ( iCount - m_iPredictedDiscards ) <= iMaxItems ) return false; if ( !pItem ) return false; // We're forcing the player to make room for items he's found. Bring up that panel with the first item over the limit. CItemDiscardPanel *pDiscardPanel = EconUI()->OpenItemDiscardPanel(); pDiscardPanel->SetItem( pItem ); return true; } //----------------------------------------------------------------------------- // Purpose: Client Acknowledges an item and moves it in to the backpack //----------------------------------------------------------------------------- void CInventoryManager::AcknowledgeItem ( CEconItemView *pItem, bool bMoveToBackpack /* = true */ ) { SetAckedByClient( pItem ); int iMethod = GetUnacknowledgedReason( pItem->GetInventoryPosition() ) - 1; if ( iMethod >= ARRAYSIZE( g_pszItemPickupMethodStringsUnloc ) || iMethod < 0 ) iMethod = 0; EconUI()->Gamestats_ItemTransaction( IE_ITEM_RECEIVED, pItem, g_pszItemPickupMethodStringsUnloc[iMethod] ); // Then move it to the first empty backpack position if ( bMoveToBackpack ) { SetItemBackpackPosition( pItem, 0, false, true ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CEconItemView *CInventoryManager::GetItemByBackpackPosition( int iBackpackPosition ) { CPlayerInventory *pInventory = GetLocalInventory(); if ( !pInventory ) return NULL; // Backpack positions start from 1 Assert( iBackpackPosition > 0 && iBackpackPosition <= pInventory->GetMaxItemCount() ); for ( int i = 0; i < pInventory->GetItemCount(); i++ ) { CEconItemView *pItem = pInventory->GetItem(i); if ( GetBackpackPositionFromBackend( pItem->GetInventoryPosition() ) == iBackpackPosition ) return pItem; } return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CInventoryManager::HasBeenAckedByClient( CEconItemView *pItem ) { return ( GetAckKeyForItem( pItem ) != NULL ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SetAckedByClient( CEconItemView *pItem ) { VerifyAckFileLoaded(); static char szTmp[128]; Q_snprintf( szTmp, sizeof(szTmp), "%llu", pItem->GetItemID() ); m_pkvItemClientAckFile->SetInt( szTmp, 1 ); m_bClientAckDirty = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SetAckedByGC( CEconItemView *pItem, bool bSave ) { KeyValues *pkvItem = GetAckKeyForItem( pItem ); if ( pkvItem ) { m_pkvItemClientAckFile->RemoveSubKey( pkvItem ); pkvItem->deleteThis(); m_bClientAckDirty = true; if ( bSave ) { SaveAckFile(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- KeyValues *CInventoryManager::GetAckKeyForItem( CEconItemView *pItem ) { VerifyAckFileLoaded(); static char szTmp[128]; Q_snprintf( szTmp, sizeof(szTmp), "%llu", pItem->GetItemID() ); return m_pkvItemClientAckFile->FindKey( szTmp ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::VerifyAckFileLoaded( void ) { if ( m_pkvItemClientAckFile ) return; m_pkvItemClientAckFile = new KeyValues( ITEM_CLIENTACK_FILE ); ISteamRemoteStorage *pRemoteStorage = SteamClient()?(ISteamRemoteStorage *)SteamClient()->GetISteamGenericInterface( SteamAPI_GetHSteamUser(), SteamAPI_GetHSteamPipe(), STEAMREMOTESTORAGE_INTERFACE_VERSION ):NULL; if ( pRemoteStorage ) { if ( pRemoteStorage->FileExists(ITEM_CLIENTACK_FILE) ) { int32 nFileSize = pRemoteStorage->GetFileSize( ITEM_CLIENTACK_FILE ); if ( nFileSize > 0 ) { CUtlBuffer buf( 0, nFileSize ); if ( pRemoteStorage->FileRead( ITEM_CLIENTACK_FILE, buf.Base(), nFileSize ) == nFileSize ) { buf.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSize ); m_pkvItemClientAckFile->ReadAsBinary( buf ); #ifdef _DEBUG if ( item_debug_clientacks.GetBool() ) { m_pkvItemClientAckFile->SaveToFile( g_pFullFileSystem, "cfg/tmp_readack.txt", "MOD" ); } #endif } } } } } //----------------------------------------------------------------------------- // Purpose: Clean up any item references that we no longer have items for. // This ensures that if we delete an item on the backend, we remove it from the ack file. //----------------------------------------------------------------------------- void CInventoryManager::CleanAckFile( void ) { CPlayerInventory *pInventory = InventoryManager()->GetLocalInventory(); if ( !pInventory ) return; if ( !pInventory->RetrievedInventoryFromSteam() ) return; if ( m_pkvItemClientAckFile ) { KeyValues *pKVItem = m_pkvItemClientAckFile->GetFirstSubKey(); while ( pKVItem != NULL ) { itemid_t ulID = (itemid_t)Q_atoi64( pKVItem->GetName() ); if ( pInventory->GetInventoryItemByItemID(ulID) == NULL ) { KeyValues *pTmp = pKVItem->GetNextKey(); m_pkvItemClientAckFile->RemoveSubKey( pKVItem ); pKVItem->deleteThis(); m_bClientAckDirty = true; pKVItem = pTmp; } else { pKVItem = pKVItem->GetNextKey(); } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CInventoryManager::SaveAckFile( void ) { if ( !m_bClientAckDirty ) return; m_bClientAckDirty = false; ISteamRemoteStorage *pRemoteStorage = SteamClient()?(ISteamRemoteStorage *)SteamClient()->GetISteamGenericInterface( SteamAPI_GetHSteamUser(), SteamAPI_GetHSteamPipe(), STEAMREMOTESTORAGE_INTERFACE_VERSION ):NULL; if ( pRemoteStorage ) { CUtlBuffer buf; m_pkvItemClientAckFile->WriteAsBinary( buf ); pRemoteStorage->FileWrite( ITEM_CLIENTACK_FILE, buf.Base(), buf.TellPut() ); #ifdef _DEBUG if ( item_debug_clientacks.GetBool() ) { m_pkvItemClientAckFile->SaveToFile( g_pFullFileSystem, "cfg/tmp_saveack.txt", "MOD" ); } #endif } } //----------------------------------------------------------------------------- // Purpose: GC sent name of account down //----------------------------------------------------------------------------- class CGCLookupAccountNameResponse : public GCSDK::CGCClientJob { public: CGCLookupAccountNameResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { GCSDK::CGCMsg msg( pNetPacket ); CUtlString playerName; if ( msg.BReadStr( &playerName ) ) { InventoryManager()->PersonaName_Store( msg.Body().m_unAccountID, playerName.Get() ); } return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGCLookupAccountNameResponse, "CGCLookupAccountNameResponse", k_EMsgGCLookupAccountNameResponse, GCSDK::k_EServerTypeGCClient ); class CGCLookupMultipleAccountsNameResponse : public GCSDK::CGCClientJob { public: CGCLookupMultipleAccountsNameResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { CProtoBufMsg msg( pNetPacket ); for ( int i = 0 ; i < msg.Body().accounts_size() ; ++i ) { const CMsgLookupMultipleAccountNamesResponse_Account &account = msg.Body().accounts( i ); InventoryManager()->PersonaName_Store( account.accountid(), account.persona().c_str() ); } return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGCLookupMultipleAccountsNameResponse, "CGCLookupMultipleAccountsNameResponse", k_EMsgGCLookupMultipleAccountNamesResponse, GCSDK::k_EServerTypeGCClient ); void CInventoryManager::PersonaName_Precache( uint32 unAccountID ) { const char *pszName = PersonaName_Get( unAccountID ); if ( pszName == NULL ) { // Queue request name from GC m_msgPendingLookupAccountNames.add_accountids( unAccountID ); // insert empty string so we don't ask again m_mapPersonaNamesCache.Insert( unAccountID, "" ); } } const char *CInventoryManager::PersonaName_Get( uint32 unAccountID ) { tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); // First ask Steam if this is one of friends -- if so we can get an up-to-date persona name. { const char *pszName = NULL; if ( steamapicontext && steamapicontext->SteamUser() && steamapicontext->SteamFriends() ) { CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); steamID.SetAccountID( unAccountID ); uint64 u64AccountId = steamID.ConvertToUint64(); // We're covering three states here: // 1. We've never asked before. We need to queue up a RequestUserInformation. // 2. We've asked before, and we haven't heard back yet // 3. We've asked before, we heard back. Don't re-request user information. auto index = m_personaNameRequests.Find( u64AccountId ); if ( !m_personaNameRequests.IsValidIndex( index ) ) { // This is case 1--we've never asked before. // If RequestUserInformation returns false, the information is already available. // Otherwise, it will arrive later and we need to rebuild the description at that time. if ( !steamapicontext->SteamFriends()->RequestUserInformation( steamID, true ) ) { pszName = steamapicontext->SteamFriends()->GetFriendPersonaName( steamID ); Assert( pszName ); // Guaranteed by the steam api if ( Q_strncmp( pszName, "[unknown]", ARRAYSIZE( "[unknown]" ) ) != 0 ) { m_mapPersonaNamesCache.InsertOrReplace( unAccountID, pszName ); return pszName; } } else { // This is case 2, we've asked above. m_personaNameRequests.Insert( u64AccountId, false ); } } else { if ( m_personaNameRequests[ index ] ) { // This is case 3. pszName = steamapicontext->SteamFriends()->GetFriendPersonaName( steamID ); Assert( pszName ); // Guaranteed by the steam api if ( Q_strncmp( pszName, "[unknown]", ARRAYSIZE( "[unknown]" ) ) != 0 ) { m_mapPersonaNamesCache.InsertOrReplace( unAccountID, pszName ); return pszName; } } } } } // If that didn't work, ask the server we're playing on if they know this account ID. CBasePlayer *pPlayer = GetPlayerByAccountID( unAccountID ); if ( pPlayer ) { const char *pszPlayerName = pPlayer->GetPlayerName(); if ( pszPlayerName ) { m_mapPersonaNamesCache.InsertOrReplace( unAccountID, pszPlayerName ); return pszPlayerName; } } // If *that* didn't work, look in our cache populated by the GC (or the above paths). This // might be out of date but it's better than nothing. int idx = m_mapPersonaNamesCache.Find( unAccountID ); if ( m_mapPersonaNamesCache.IsValidIndex( idx ) ) { return m_mapPersonaNamesCache[idx].Get(); } return "[unknown]"; } void CInventoryManager::PersonaName_Store( uint32 unAccountID, const char *pPersonaName ) { m_mapPersonaNamesCache.InsertOrReplace( unAccountID, pPersonaName ); } #endif // CLIENT_DLL //======================================================================================================================= // PLAYER INVENTORY //======================================================================================================================= //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CPlayerInventory::CPlayerInventory( void ) { m_bGotItemsFromSteam = false; m_iPendingRequests = 0; m_aInventoryItems.Purge(); m_pSOCache = NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CPlayerInventory::~CPlayerInventory() { FOR_EACH_VEC( m_vecItemHandles, i ) { m_vecItemHandles[ i ]->InventoryIsBeingDeleted(); } m_vecItemHandles.Purge(); if ( m_iPendingRequests ) { InventoryManager()->RemovePendingRequest( &m_OwnerID ); m_iPendingRequests = 0; } SOClear(); InventoryManager()->DeregisterInventory( this ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::SOClear() { if ( m_OwnerID.IsValid() ) { CGCClientSystem *pClientSystem = GCClientSystem(); Assert ( pClientSystem != NULL ); if ( pClientSystem != NULL ) { CGCClient *pClient = pClientSystem->GetGCClient(); Assert ( pClient != NULL ); pClient->RemoveSOCacheListener( m_OwnerID, this ); } } // Somebody registered as a listener through us, but now our Steam ID // is changing? This is bad news. Assert( m_vecListeners.Count() == 0 ); while ( m_vecListeners.Count() > 0 ) { RemoveListener( m_vecListeners[0] ); } // If we were subscribed, we should have gotten our unsubscribe message, // and that should have cleared the pointer Assert( m_pSOCache == NULL); m_pSOCache = NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::AddItemHandle( CEconItemViewHandle* pHandle ) { FOR_EACH_VEC( m_vecItemHandles, i ) { if ( m_vecItemHandles[ i ] == pHandle ) { Assert( !"Item handle already in list to track!" ); return; } } m_vecItemHandles.AddToTail( pHandle ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::RemoveItemHandle( CEconItemViewHandle* pHandle ) { FOR_EACH_VEC( m_vecItemHandles, i ) { if ( m_vecItemHandles[ i ] == pHandle ) { m_vecItemHandles.Remove( i ); return; } } Assert( !"Could not find item handle to remove!" ); } void CPlayerInventory::Clear() { SOClear(); m_OwnerID = CSteamID(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::RequestInventory( CSteamID pSteamID ) { // Make sure we don't already have somebody else's stuff // on hand if ( m_OwnerID != pSteamID ) SOClear(); // Remember whose inventory we're looking at m_OwnerID = pSteamID; // SteamID must be valid if ( !m_OwnerID.IsValid() || !m_OwnerID.BIndividualAccount() ) { Assert( m_OwnerID.IsValid() ); Assert( m_OwnerID.BIndividualAccount() ); return; } // If we don't already have an SO cache, then ask the GC for one, // and start listening to it. We will receive our "subscribed" message // when the data is valid GCClientSystem()->GetGCClient()->AddSOCacheListener( m_OwnerID, this ); } void CPlayerInventory::AddListener( GCSDK::ISharedObjectListener *pListener ) { Assert( m_OwnerID.IsValid() ); if ( m_vecListeners.Find( pListener ) < 0 ) { m_vecListeners.AddToTail( pListener ); GCClientSystem()->GetGCClient()->AddSOCacheListener( m_OwnerID, pListener ); } } void CPlayerInventory::RemoveListener( GCSDK::ISharedObjectListener *pListener ) { if ( m_OwnerID.IsValid() ) { m_vecListeners.FindAndFastRemove( pListener ); GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_OwnerID, pListener ); } else { Assert( m_vecListeners.Count() == 0 ); } } //----------------------------------------------------------------------------- // Purpose: Helper function to add a new item for a econ item //----------------------------------------------------------------------------- bool CPlayerInventory::AddEconItem( CEconItem * pItem, bool bUpdateAckFile, bool bWriteAckFile, bool bCheckForNewItems ) { CEconItemView newItem; if( !FilloutItemFromEconItem( &newItem, pItem ) ) { return false; } int iIdx = m_aInventoryItems.Insert( newItem ); DirtyItemHandles(); ItemHasBeenUpdated( &m_aInventoryItems[iIdx], bUpdateAckFile, bWriteAckFile ); #ifdef CLIENT_DLL if ( bCheckForNewItems && InventoryManager()->GetLocalInventory() == this ) { bool bNotify = IsUnacknowledged( pItem->GetInventoryToken() ); // ignore Halloween drops bNotify &= pItem->GetOrigin() != kEconItemOrigin_HalloweenDrop; // only notify for specific reasons unacknowledged_item_inventory_positions_t reason = GetUnacknowledgedReason( pItem->GetInventoryToken() ); switch ( reason ) { case UNACK_ITEM_UNKNOWN: case UNACK_ITEM_DROPPED: case UNACK_ITEM_SUPPORT: case UNACK_ITEM_EARNED: case UNACK_ITEM_REFUNDED: case UNACK_ITEM_COLLECTION_REWARD: case UNACK_ITEM_TRADED: case UNACK_ITEM_GIFTED: case UNACK_ITEM_QUEST_LOANER: case UNACK_ITEM_VIRAL_COMPETITIVE_BETA_PASS_SPREAD: break; default: bNotify = false; break; } if ( bNotify && !pItem->GetItemDefinition()->IsHidden() ) { OnHasNewItems(); } } #endif return true; } //----------------------------------------------------------------------------- // Purpose: Creates a script item and associates it with this econ item //----------------------------------------------------------------------------- void CPlayerInventory::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); if( pObject->GetTypeID() != CEconItem::k_nTypeID ) return; Assert( steamIDOwner == m_OwnerID ); if ( steamIDOwner != m_OwnerID ) return; // We shouldn't get these notifications unless we're subscribed, right? if ( m_pSOCache == NULL) { Assert( m_pSOCache ); return; } // Don't bother unless it's an incremental notification. // For mass updates, we'll do everything more efficiently in one place if ( eEvent != GCSDK::eSOCacheEvent_Incremental ) { Assert( eEvent == GCSDK::eSOCacheEvent_Subscribed || eEvent == GCSDK::eSOCacheEvent_Resubscribed || eEvent == GCSDK::eSOCacheEvent_ListenerAdded ); return; } CEconItem *pItem = (CEconItem *)pObject; AddEconItem( pItem, true, true, true ); SendInventoryUpdateEvent(); } //----------------------------------------------------------------------------- // Purpose: Updates the script item associated with this econ item //----------------------------------------------------------------------------- void CPlayerInventory::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { if( pObject->GetTypeID() != CEconItem::k_nTypeID ) return; Assert( steamIDOwner == m_OwnerID ); if ( steamIDOwner != m_OwnerID ) return; // We shouldn't get these notifications unless we're subscribed, right? if ( m_pSOCache == NULL) { Assert( m_pSOCache ); return; } // Don't bother unless it's an incremental notification. // For mass updates, we'll do everything more efficiently in one place if ( eEvent != GCSDK::eSOCacheEvent_Incremental ) { Assert( eEvent == GCSDK::eSOCacheEvent_Subscribed || eEvent == GCSDK::eSOCacheEvent_Resubscribed ); return; } CEconItem *pEconItem = (CEconItem *)pObject; bool bChanged = false; CEconItemView *pScriptItem = GetInventoryItemByItemID( pEconItem->GetItemID() ); if ( pScriptItem ) { if ( FilloutItemFromEconItem( pScriptItem, pEconItem ) ) { ItemHasBeenUpdated( pScriptItem, false, false ); } bChanged = true; } else { // The item isn't in this inventory right now. But it may need to be // after the update, so try adding it and see if the inventory wants it. bChanged = AddEconItem( pEconItem, false, false, false ); } if ( bChanged ) { ResortInventory(); DirtyItemHandles(); #ifdef CLIENT_DLL // Client doesn't update inventory while items are moving in a backpack sort. Does it once at the sort end instead. if ( !InventoryManager()->IsInBackpackSort() ) #endif { SendInventoryUpdateEvent(); } #ifdef _DEBUG if ( item_inventory_debug.GetBool() ) { DumpInventoryToConsole( true ); } #endif } } //----------------------------------------------------------------------------- // Purpose: Removes the script item associated with this econ item //----------------------------------------------------------------------------- void CPlayerInventory::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { if( pObject->GetTypeID() != CEconItem::k_nTypeID ) return; Assert( steamIDOwner == m_OwnerID ); if ( steamIDOwner != m_OwnerID ) return; // We shouldn't get these notifications unless we're subscribed, right? if ( m_pSOCache == NULL) { Assert( m_pSOCache ); return; } // Don't bother unless it's an incremental notification. // For mass updates, we'll do everything more efficiently in one place if ( eEvent != GCSDK::eSOCacheEvent_Incremental ) { Assert( eEvent == GCSDK::eSOCacheEvent_Subscribed || eEvent == GCSDK::eSOCacheEvent_Resubscribed ); return; } CEconItem *pEconItem = (CEconItem *)pObject; RemoveItem( pEconItem->GetItemID() ); #ifdef CLIENT_DLL InventoryManager()->OnItemDeleted( this ); #endif SendInventoryUpdateEvent(); } //----------------------------------------------------------------------------- // Purpose: This is our initial notification that this cache has been received // from the server. //----------------------------------------------------------------------------- void CPlayerInventory::SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) { // Make sure we expect notifications about this guy Assert( steamIDOwner == m_OwnerID ); if ( steamIDOwner != m_OwnerID ) return; #ifdef _DEBUG Msg("CPlayerInventory::SOCacheSubscribed\n"); #endif // Clear our old inventory m_aInventoryItems.Purge(); DirtyItemHandles(); // Locate the cache that was just subscribed to m_pSOCache = GCClientSystem()->GetSOCache( m_OwnerID ); if ( m_pSOCache == NULL ) { Assert( m_pSOCache != NULL ); return; } // add all the items already in the inventory CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindTypeCache( CEconItem::k_nTypeID ); if( pTypeCache ) { for( uint32 unItem = 0; unItem < pTypeCache->GetCount(); unItem++ ) { CEconItem *pItem = (CEconItem *)pTypeCache->GetObject( unItem ); AddEconItem(pItem, true, false, true ); } } m_bGotItemsFromSteam = true; #ifdef CLIENT_DLL if ( InventoryManager()->GetLocalInventory() == this ) { // Only validate the local player inventory ValidateInventoryPositions(); // tell the entire client that we're 'connected' to the GC now CInventoryManager::SendGCConnectedEvent(); } #endif ResortInventory(); #ifdef CLIENT_DLL // Now that we've read all the items in, write out the ack file (only if we're the local inventory) if ( InventoryManager()->GetLocalInventory() == this ) { InventoryManager()->CleanAckFile(); InventoryManager()->SaveAckFile(); } #endif } bool CInventoryManager::IsValidPlayerClass( equipped_class_t unClass ) { const bool bResult = ItemSystem()->GetItemSchema()->IsValidClass( unClass ); AssertMsg( bResult, "Invalid player class!" ); return bResult; } //----------------------------------------------------------------------------- // Purpose: Removes the script item associated with this econ item //----------------------------------------------------------------------------- void CPlayerInventory::ValidateInventoryPositions( void ) { #ifdef TF2 if ( engine->GetAppID() == 520 ) { TFInventoryManager()->DeleteUnknowns( this ); } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::ItemHasBeenUpdated( CEconItemView *pItem, bool bUpdateAckFile, bool bWriteAckFile ) { #ifdef CLIENT_DLL // Handle the clientside ack file if ( bUpdateAckFile && !IsUnacknowledged(pItem->GetInventoryPosition()) ) { if ( InventoryManager()->GetLocalInventory() == this ) { InventoryManager()->SetAckedByGC( pItem, bWriteAckFile ); } } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::SOCacheUnsubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) { m_pSOCache = NULL; m_bGotItemsFromSteam = false; m_aInventoryItems.Purge(); DirtyItemHandles(); } //----------------------------------------------------------------------------- // Purpose: On the client this sends the "inventory_updated" event. On the server // it does nothing. //----------------------------------------------------------------------------- void CPlayerInventory::SendInventoryUpdateEvent() { #ifdef CLIENT_DLL if( InventoryManager()->GetLocalInventory() == this ) { IGameEvent *event = gameeventmanager->CreateEvent( "inventory_updated" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } } #endif } //----------------------------------------------------------------------------- // Purpose: Fills out all the fields in the script item based on what's in the // econ item //----------------------------------------------------------------------------- bool CPlayerInventory::FilloutItemFromEconItem( CEconItemView *pScriptItem, CEconItem *pEconItem ) { // We need to detect the case where items have been updated & moved bags / positions. uint32 iOldPos = pScriptItem->GetInventoryPosition(); bool bWasInThisBag = ItemShouldBeIncluded( iOldPos ); // Ignore items that this inventory doesn't care about if ( !ItemShouldBeIncluded( pEconItem->GetInventoryToken() ) ) { // The item has been moved out of this bag. Ensure our derived inventory classes know. if ( bWasInThisBag ) { // We need to update it before it's removed. ItemHasBeenUpdated( pScriptItem, false, false ); RemoveItem( pEconItem->GetItemID() ); } return false; } pScriptItem->Init( pEconItem->GetDefinitionIndex(), pEconItem->GetQuality(), pEconItem->GetItemLevel(), pEconItem->GetAccountID() ); if ( !pScriptItem->IsValid() ) return false; pScriptItem->SetItemID( pEconItem->GetItemID() ); pScriptItem->SetInventoryPosition( pEconItem->GetInventoryToken() ); OnItemChangedPosition( pScriptItem, iOldPos ); #if BUILD_ITEM_NAME_AND_DESC // Precache account names if we have any. We do this way in advance of any code that might // use it (ie., description text building) so that by the time we try that we already have // the data setup. // // We don't worry about yielding here because this inventory code only runs on game // clients/servers, not the GC. CSteamAccountIDAttributeCollector AccountIDCollector; pEconItem->IterateAttributes( &AccountIDCollector ); FOR_EACH_VEC( AccountIDCollector.GetAccountIDs(), i ) { InventoryManager()->PersonaName_Precache( (AccountIDCollector.GetAccountIDs())[i] ); } #endif return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::DumpInventoryToConsole( bool bRoot ) { if ( bRoot ) { #ifdef CLIENT_DLL Msg("(CLIENT) Inventory:\n"); #else Msg("(SERVER) Inventory for account (%d):\n", m_OwnerID.GetAccountID() ); #endif Msg(" Version: %llu:\n", m_pSOCache ? m_pSOCache->GetVersion() : -1 ); } int iCount = m_aInventoryItems.Count(); Msg(" Num items: %d\n", iCount ); for ( int i = 0; i < iCount; i++ ) { Msg(" %s (ID %llu)\n", m_aInventoryItems[i].GetStaticData()->GetDefinitionName(), m_aInventoryItems[i].GetItemID() ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPlayerInventory::RemoveItem( itemid_t iItemID ) { int iIndex; CEconItemView *pItem = GetInventoryItemByItemID( iItemID, &iIndex ); if ( pItem ) { ItemIsBeingRemoved( pItem ); FOR_EACH_VEC( m_vecItemHandles, i ) { m_vecItemHandles[ i ]->MarkDirty(); m_vecItemHandles[ i ]->ItemIsBeingDeleted( pItem ); } m_aInventoryItems.Remove(iIndex); #ifdef _DEBUG if ( item_inventory_debug.GetBool() ) { DumpInventoryToConsole( true ); } #endif } // Don't need to resort because items will still be in order } //----------------------------------------------------------------------------- // Purpose: Finds the item in our inventory that matches the specified global index //----------------------------------------------------------------------------- CEconItemView *CPlayerInventory::GetInventoryItemByItemID( itemid_t iIndex, int *pIndex ) { int iCount = m_aInventoryItems.Count(); for ( int i = 0; i < iCount; i++ ) { if ( m_aInventoryItems[i].GetItemID() == iIndex ) { if ( pIndex ) { *pIndex = i; } return &m_aInventoryItems[i]; } } return NULL; } //----------------------------------------------------------------------------- // Finds the item in our inventory that matches the specified global original id //----------------------------------------------------------------------------- CEconItemView *CPlayerInventory::GetInventoryItemByOriginalID( itemid_t iOriginalID, int *pIndex /*= NULL*/ ) { int iCount = m_aInventoryItems.Count(); for ( int i = 0; i < iCount; i++ ) { CEconItem *pItem = m_aInventoryItems[i].GetSOCData(); if ( pItem && pItem->GetOriginalID() == iOriginalID ) { if ( pIndex ) { *pIndex = i; } return &m_aInventoryItems[i]; } } return NULL; } //----------------------------------------------------------------------------- // Purpose: Finds the item in our inventory in the specified position //----------------------------------------------------------------------------- CEconItemView *CPlayerInventory::GetItemByPosition( int iPosition, int *pIndex ) { int iCount = m_aInventoryItems.Count(); for ( int i = 0; i < iCount; i++ ) { if ( m_aInventoryItems[i].GetInventoryPosition() == (unsigned int)iPosition ) { if ( pIndex ) { *pIndex = i; } return &m_aInventoryItems[i]; } } return NULL; } // Finds the first item in our backpack with match itemdef //----------------------------------------------------------------------------- CEconItemView *CPlayerInventory::FindFirstItembyItemDef( item_definition_index_t iItemDef ) { int iCount = m_aInventoryItems.Count(); for ( int i = 0; i < iCount; i++ ) { //GetItemDefIndex() if ( m_aInventoryItems[i].GetItemDefIndex() == iItemDef ) { return &m_aInventoryItems[i]; } } return NULL; } //----------------------------------------------------------------------------- // Purpose: Get the index for the item in our inventory utlvector //----------------------------------------------------------------------------- int CPlayerInventory::GetIndexForItem( CEconItemView *pItem ) { int iCount = m_aInventoryItems.Count(); for ( int i = 0; i < iCount; i++ ) { if ( m_aInventoryItems[i].GetItemID() == pItem->GetItemID() ) return i; } return -1; } //----------------------------------------------------------------------------- // Purpose: Dirty all the item handles that are registered with us //----------------------------------------------------------------------------- void CPlayerInventory::DirtyItemHandles() { FOR_EACH_VEC( m_vecItemHandles, i ) { m_vecItemHandles[ i ]->MarkDirty(); } } //----------------------------------------------------------------------------- // Purpose: Get the item object cache data for the specified item //----------------------------------------------------------------------------- CEconItem *CPlayerInventory::GetSOCDataForItem( itemid_t iItemID ) { if ( !m_pSOCache ) return NULL; CEconItem soIndex; soIndex.SetItemID( iItemID ); return (CEconItem *)m_pSOCache->FindSharedObject( soIndex ); } #if defined (_DEBUG) && defined(CLIENT_DLL) CON_COMMAND_F( item_deleteall, "WARNING: Removes all of the items in your inventory.", FCVAR_CHEAT ) { CPlayerInventory *pInventory = InventoryManager()->GetLocalInventory(); if ( !pInventory ) return; int iCount = pInventory->GetItemCount(); for ( int i = 0; i < iCount; i++ ) { CEconItemView *pItem = pInventory->GetItem(i); if ( pItem ) { InventoryManager()->DropItem( pItem->GetItemID() ); } } InventoryManager()->UpdateLocalInventory(); } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CPlayerInventory::GetRecipeCount() const { const CUtlMap& mapRecipes = ItemSystem()->GetItemSchema()->GetRecipeDefinitionMap(); return mapRecipes.Count(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const CEconCraftingRecipeDefinition *CPlayerInventory::GetRecipeDef( int iIndex ) { if ( !m_pSOCache ) return NULL; if ( iIndex < 0 || iIndex >= GetRecipeCount() ) return NULL; const CEconItemSchema::RecipeDefinitionMap_t& mapRecipes = GetItemSchema()->GetRecipeDefinitionMap(); // Store off separate index for "number of items iterated over" in case something // deletes from the recipes map out from under us. int j = 0; FOR_EACH_MAP_FAST( mapRecipes, i ) { if ( j == iIndex ) return mapRecipes[i]; j++; } return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const CEconCraftingRecipeDefinition *CPlayerInventory::GetRecipeDefByDefIndex( uint16 iDefIndex ) { if ( !m_pSOCache ) return NULL; // check always-known recipes const CUtlMap& mapRecipes = ItemSystem()->GetItemSchema()->GetRecipeDefinitionMap(); int i = mapRecipes.Find( iDefIndex ); if ( i != mapRecipes.InvalidIndex() ) return mapRecipes[i]; // there are no more SO recipes return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CEconItemViewHandle::SetItem( CEconItemView* pItem ) { m_pItem = pItem; if ( pItem ) { // Cache the item_id for lookup when our pointer gets dirtied m_nItemID = pItem->GetItemID(); auto* pInv = InventoryManager()->GetInventoryForAccount( pItem->GetAccountID() ); Assert( pInv ); if ( m_pInv != pInv ) { // If this is a different inventory, unsubscribe. This can happen if the // handle gets reused if ( m_pInv ) { m_pInv->RemoveItemHandle( this ); } m_pInv = pInv; // Subscribe to the new inventory m_pInv->AddItemHandle( this ); } } } //----------------------------------------------------------------------------- // Purpose: Return a pointer to a CEconItemView //----------------------------------------------------------------------------- CEconItemView* CEconItemViewHandle::Get() const { // If our pointer is dirty, we need to go get a new pointer if ( m_bPointerDirty ) { if ( m_pInv ) { m_pItem = m_pInv->GetInventoryItemByItemID( m_nItemID ); m_bPointerDirty = false; } } return m_pItem; } //----------------------------------------------------------------------------- // Purpose: Unsubscribe us from future updates //----------------------------------------------------------------------------- CEconItemHandle::~CEconItemHandle() { UnsubscribeFromSOEvents(); } //----------------------------------------------------------------------------- // Purpose: Save a pointer to the item and register us for SOCache events //----------------------------------------------------------------------------- void CEconItemHandle::SetItem( CEconItem* pItem ) { UnsubscribeFromSOEvents(); m_pItem = NULL; m_iItemID = INVALID_ITEM_ID; if ( pItem ) { auto* pInv = InventoryManager()->GetInventoryForAccount( pItem->GetAccountID() ); if ( pInv ) { m_OwnerSteamID.SetFromUint64( pInv->GetOwner().ConvertToUint64() ); GCClientSystem()->GetGCClient()->AddSOCacheListener( m_OwnerSteamID, this ); } m_pItem = pItem; m_iItemID = pItem->GetID(); } } //----------------------------------------------------------------------------- // Purpose: Check if out item got deleted. If it did, mark our pointer as NULL // so future dereferences will get NULL instead of a stale pointer. //----------------------------------------------------------------------------- void CEconItemHandle::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { if( pObject->GetTypeID() != CEconItem::k_nTypeID || m_pItem == NULL ) return; const CEconItem *pItem = (CEconItem *)pObject; if ( m_iItemID == pItem->GetID() ) { UnsubscribeFromSOEvents(); m_pItem = NULL; m_iItemID = INVALID_ITEM_ID; } } void CEconItemHandle::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { if( pObject->GetTypeID() != CEconItem::k_nTypeID ) return; CEconItem *pItem = (CEconItem *)pObject; if ( m_iItemID == pItem->GetID() ) { SetItem( pItem ); } } void CEconItemHandle::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { if ( pObject->GetTypeID() != CEconItem::k_nTypeID ) return; CEconItem *pItem = (CEconItem *)pObject; if ( m_iItemID == pItem->GetID() ) { SetItem( pItem ); } } void CEconItemHandle::SOCacheUnsubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) { UnsubscribeFromSOEvents(); } void CEconItemHandle::UnsubscribeFromSOEvents() { if ( m_OwnerSteamID.GetAccountID() != 0 ) { GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_OwnerSteamID, this ); } } #if defined( STAGING_ONLY ) || defined( _DEBUG ) #if defined(CLIENT_DLL) CON_COMMAND_F( item_dumpinv, "Dumps the contents of a specified client inventory.", FCVAR_CHEAT ) #else CON_COMMAND_F( item_dumpinv_sv, "Dumps the contents of a specified server inventory.", FCVAR_CHEAT ) #endif { #if defined(CLIENT_DLL) CPlayerInventory *pInventory = InventoryManager()->GetLocalInventory(); #else CSteamID steamID; CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_GetCommandClient() ); pPlayer->GetSteamID( &steamID ); CPlayerInventory *pInventory = InventoryManager()->GetInventoryForAccount( steamID.GetAccountID() ); #endif if ( !pInventory ) { Msg("No inventory found.\n"); return; } pInventory->DumpInventoryToConsole( true ); } #if defined (CLIENT_DLL) CON_COMMAND_F( item_dumpschema, "Dump the expanded schema for items to a file in sorted order suitable for diffs. Format: item_dumpschema ", FCVAR_CHEAT ) { if ( args.ArgC() != 2 ) { Msg("Usage: item_dumpschema \n"); return; } if ( GetItemSchema()->DumpItems(args[1]) ) Msg("Dump complete, saved in game/tf/%s\n", args[1]); else Msg("Dump failed (?)\n"); } CON_COMMAND_F( item_giveitem, "Give an item to the local player. Format: item_giveitem or ", FCVAR_NONE ) { if ( !steamapicontext || !steamapicontext->SteamUser() ) { Msg("Not connected to Steam.\n"); return; } CSteamID steamIDForPlayer = steamapicontext->SteamUser()->GetSteamID(); if ( !steamIDForPlayer.IsValid() ) { Msg("Failed to find a valid steamID for the local player.\n"); return; } int iItemCount = args.ArgC(); for ( int i = 1; i < iItemCount; ++i ) { // Check to see if args[1] is a number (itemdefid) and if so, translate it to actual itemname const char *pszItemname = NULL; if ( V_isdigit( args[i][0] ) ) { int iDef = V_atoi( args[i] ); CEconItemDefinition *pItemDef = GetItemSchema()->GetItemDefinition( iDef ); if ( pItemDef ) { pszItemname = pItemDef->GetItemDefinitionName(); } } else { pszItemname = args[i]; } Msg("Sending request to generate '%s' for Local Player (%llu)\n", pszItemname, steamIDForPlayer.ConvertToUint64() ); CItemSelectionCriteria criteria; GCSDK::CProtoBufMsg msg( k_EMsgGCDev_NewItemRequest ); msg.Body().set_receiver( steamIDForPlayer.ConvertToUint64() ); criteria.SetIgnoreEnabledFlag( true ); if ( !criteria.BAddCondition( "name", k_EOperator_String_EQ, pszItemname, true ) || !criteria.BSerializeToMsg( *msg.Body().mutable_criteria() ) ) { Msg("Failed to add condition and/or serialize item grant request. This is probably caused by having a string that's too long.\n" ); return; } GCClientSystem()->BSendMessage( msg ); } } CON_COMMAND_F( item_rolllootlist, "Force a loot list rool for the local player. Format: item_rolllootlist ", FCVAR_NONE ) { if ( !steamapicontext || !steamapicontext->SteamUser() ) { Msg("Not connected to Steam.\n"); return; } CSteamID steamIDForPlayer = steamapicontext->SteamUser()->GetSteamID(); if ( !steamIDForPlayer.IsValid() ) { Msg("Failed to find a valid steamID for the local player.\n"); return; } Msg("Sending request to roll '%s' for Local Player (%llu)\n", args[1], steamIDForPlayer.ConvertToUint64() ); GCSDK::CProtoBufMsg msg( k_EMsgGCDev_DebugRollLootRequest ); msg.Body().set_receiver( steamIDForPlayer.ConvertToUint64() ); msg.Body().set_loot_list_name( args[1] ); GCClientSystem()->BSendMessage( msg ); } #include "econ_item_description.h" #include "localization_provider.h" CON_COMMAND_F( item_generate_all_descriptions, "Generate full item descriptions for every item in your backpack. Meant as a code test.", FCVAR_CHEAT ) { CPlayerInventory *pInventory = InventoryManager()->GetLocalInventory(); for ( int i = 0; i < pInventory->GetItemCount(); i++ ) { CEconItemDescription desc; IEconItemDescription::YieldingFillOutEconItemDescription( &desc, GLocalizationProvider(), pInventory->GetItem( i ) ); } Msg("Done.\n"); } #endif // CLIENT_DLL #endif // STAGING_ONLY || _DEBUG