diff options
Diffstat (limited to 'game/shared/tf/tf_item_inventory.cpp')
| -rw-r--r-- | game/shared/tf/tf_item_inventory.cpp | 1819 |
1 files changed, 1819 insertions, 0 deletions
diff --git a/game/shared/tf/tf_item_inventory.cpp b/game/shared/tf/tf_item_inventory.cpp new file mode 100644 index 0000000..57c674a --- /dev/null +++ b/game/shared/tf/tf_item_inventory.cpp @@ -0,0 +1,1819 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "tf_item_inventory.h" +#include "econ_entity_creation.h" +#include "tf_item_system.h" +#include "vgui/ILocalize.h" +#include "tier3/tier3.h" +#ifdef CLIENT_DLL +#include "c_tf_player.h" +#include "item_pickup_panel.h" +#include "KeyValues.h" +#include "filesystem.h" +#include "character_info_panel.h" +#include "ienginevgui.h" +#include "c_tf_gamestats.h" +#include "econ_notifications.h" +#include "achievementmgr.h" +#include "baseachievement.h" +#include "achievements_tf.h" +#include "econ/econ_item_preset.h" +#include "tf_shared_content_manager.h" +#include "c_playerresource.h" +#include "quest_log_panel.h" +#include "backpack_panel.h" +#include "materialsystem/itexture.h" +#else +#include "tf_player.h" +#endif +#include "gc_clientsystem.h" +#include "econ_game_account_client.h" +#include "gcsdk/gcclientsdk.h" +#include "econ_gcmessages.h" +#include "tf_gamerules.h" +#include "tf_gcmessages.h" +#include "econ_item.h" +#include "game_item_schema.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +using namespace GCSDK; + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +CEconNotification_HasNewItems::CEconNotification_HasNewItems() : CEconNotification() +{ + m_bHasTriggered = false; + m_flExpireTime = -1.0f; // Does not initially expire + + m_bShowInGame = false; + + // Check to see if any items are not drops, if so show in game + // do not show in game if the new items are only drops + CPlayerInventory *pLocalInv = InventoryManager()->GetLocalInventory(); + if ( pLocalInv ) + { + // 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 ( InventoryManager()->GetBackpackPositionFromBackend(iPosition) != 0 ) + continue; + + // Now make sure we haven't got a clientside saved ack for this item. + if ( InventoryManager()->HasBeenAckedByClient( pTmp ) ) + continue; + + // If item is not a drop we want to show the notification otherwise they'll get the notification on death + int iFoundMethod = GetUnacknowledgedReason( iPosition ); + if ( iFoundMethod > UNACK_ITEM_DROPPED ) + { + m_bShowInGame = true; + break; + } + } + } +} + + +CEconNotification_HasNewItems::~CEconNotification_HasNewItems() +{ + if ( !m_bHasTriggered ) + { + m_bHasTriggered = true; + TFInventoryManager()->ShowItemsPickedUp( true, true, true ); + } +} + +//----------------------------------------------------------------------------- +CEconNotification_HasNewItemsOnKill::CEconNotification_HasNewItemsOnKill( int iVictimID ) +{ + m_bHasTriggered = false; + SetLifetime( 3.0f ); + SetText( "#TF_EnemyDroppedItem" ); + + wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( g_PR->GetPlayerName( iVictimID ), wszPlayerName, sizeof( wszPlayerName ) ); + AddStringToken( "victim", wszPlayerName ); + + m_bShowInGame = true; +} + +/*static*/ bool CEconNotification_HasNewItemsOnKill::HasUnacknowledgedItems () +{ + // Check to see if any items are not drops, if so show in game + // do not show in game if the new items are only drops + CPlayerInventory *pLocalInv = InventoryManager()->GetLocalInventory(); + if ( pLocalInv ) + { + // 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 ( InventoryManager()->GetBackpackPositionFromBackend( iPosition ) != 0 ) + continue; + + // Now make sure we haven't got a clientside saved ack for this item. + if ( InventoryManager()->HasBeenAckedByClient( pTmp ) ) + continue; + + // If item is not a drop we want to show the notification otherwise they'll get the notification on death + int iFoundMethod = GetUnacknowledgedReason( iPosition ); + if ( iFoundMethod > UNACK_ITEM_DROPPED ) + { + return true; + } + } + } + return false; +} + + +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool AreSlotsConsideredIdentical( EEquipType_t eEquipType, int iBaseSlot, int iTestSlot ) +{ + if ( eEquipType == EQUIP_TYPE_CLASS ) + { + if ( iBaseSlot == LOADOUT_POSITION_MISC ) + { + return IsMiscSlot( iTestSlot ); + } + + if ( iBaseSlot == LOADOUT_POSITION_BUILDING ) + { + return IsBuildingSlot( iTestSlot ); + } + + if ( iBaseSlot == LOADOUT_POSITION_TAUNT ) + { + return IsTauntSlot( iTestSlot ); + } + } + else if ( eEquipType == EQUIP_TYPE_ACCOUNT ) + { + if ( iBaseSlot == ACCOUNT_LOADOUT_POSITION_ACCOUNT1 ) + { + return IsQuestSlot( iTestSlot ); + } + } + + return iBaseSlot == iTestSlot; +} + +//----------------------------------------------------------------------------- +CTFInventoryManager g_TFInventoryManager; +CInventoryManager *InventoryManager( void ) +{ + return &g_TFInventoryManager; +} +CTFInventoryManager *TFInventoryManager( void ) +{ + return &g_TFInventoryManager; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFInventoryManager::CTFInventoryManager( void ) + +{ +} + +CTFInventoryManager::~CTFInventoryManager( void ) +{ + m_pBaseLoadoutItems.PurgeAndDeleteElements(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFInventoryManager::PostInit( void ) +{ + BaseClass::PostInit(); + GenerateBaseItems(); +} + +//----------------------------------------------------------------------------- +// Purpose: Generate & store the base item details for each class & loadout slot +//----------------------------------------------------------------------------- +void CTFInventoryManager::GenerateBaseItems( void ) +{ + // Purge our lists and make new + m_pBaseLoadoutItems.PurgeAndDeleteElements(); + + // Load a base top level invalid item + { + m_pDefaultItem = new CEconItemView; + m_pDefaultItem->Invalidate(); + } + // + const CEconItemSchema::BaseItemDefinitionMap_t& mapItems = GetItemSchema()->GetBaseItemDefinitionMap(); + int iStart = 0; + for ( int it = iStart; it != mapItems.InvalidIndex(); it = mapItems.NextInorder( it ) ) + { + CEconItemView *pItem = new CEconItemView; + pItem->Init( mapItems[it]->GetDefinitionIndex(), AE_USE_SCRIPT_VALUE, AE_USE_SCRIPT_VALUE, false ); + m_pBaseLoadoutItems.AddToTail( pItem ); + } +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFInventoryManager::EquipItemInLoadout( int iClass, int iSlot, itemid_t iItemID ) +{ + if ( !steamapicontext || !steamapicontext->SteamUser() ) + return false; + + // If they pass in a INVALID_ITEM_ID item id, we're just clearing the loadout slot + if ( iItemID == INVALID_ITEM_ID ) + return m_LocalInventory.ClearLoadoutSlot( iClass, iSlot ); + + CEconItemView *pItem = m_LocalInventory.GetInventoryItemByItemID( iItemID ); + if ( !pItem ) + return false; + + // We check for validity on the GC when we equip items, but we can't really trust anyone + // and so we check here as well. + if ( !AreSlotsConsideredIdentical( pItem->GetStaticData()->GetEquipType(), pItem->GetStaticData()->GetLoadoutSlot(iClass), iSlot ) ) + { + return false; + } + + if ( !pItem->GetStaticData()->CanBeUsedByClass( iClass ) ) + { + return false; + } + + // Equip the new item + UpdateInventoryEquippedState( &m_LocalInventory, iItemID, iClass, iSlot ); + + // TODO: Prediction + // Item has been moved, so update our loadout. + //m_LoadoutItems[iClass][iSlot] = iItemID; + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Fills out pList with all inventory items that could fit into the specified loadout slot for a given class +//----------------------------------------------------------------------------- +int CTFInventoryManager::GetAllUsableItemsForSlot( int iClass, int iSlot, CUtlVector<CEconItemView*> *pList ) +{ + bool bIsAccountIndex = iClass == GEconItemSchema().GetAccountIndex(); + if ( bIsAccountIndex ) + { + Assert( IsQuestSlot( iSlot ) ); + } + else + { + Assert( iClass >= TF_FIRST_NORMAL_CLASS && iClass < TF_CLASS_COUNT ); + Assert( iSlot >= -1 && iSlot < CLASS_LOADOUT_POSITION_COUNT ); + } + + int iCount = m_LocalInventory.GetItemCount(); + for ( int i = 0; i < iCount; i++ ) + { + CEconItemView *pItem = m_LocalInventory.GetItem(i); + CTFItemDefinition *pItemData = pItem->GetStaticData(); + + if ( bIsAccountIndex != ( pItemData->GetEquipType() == EEquipType_t::EQUIP_TYPE_ACCOUNT ) ) + continue; + + if ( !bIsAccountIndex && !pItemData->CanBeUsedByClass(iClass) ) + continue; + + // Passing in iSlot of -1 finds all items usable by the class + if ( iSlot >= 0 && pItem->GetStaticData()->GetLoadoutSlot( iClass ) != iSlot ) + continue; + + // Ignore unpack'd items + if ( IsUnacknowledged( pItem->GetInventoryPosition() ) ) + continue; + + pList->AddToTail( m_LocalInventory.GetItem(i) ); + } + + return pList->Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: Fills out pList with all quest item in the local inventory +//----------------------------------------------------------------------------- +int CTFInventoryManager::GetAllQuestItems( CUtlVector<CEconItemView*> *pList ) +{ + if ( !pList ) + return 0; + + pList->RemoveAll(); + + for ( int i = 0 ; i < m_LocalInventory.GetItemCount() ; ++i ) + { + CEconItemView *pItem = m_LocalInventory.GetItem( i ); + + // Cheapest test + if ( pItem->GetItemDefinition()->GetQuestDef() == NULL ) + continue; + + pList->AddToTail( pItem ); + } + + return pList->Count(); +} + +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEconItemView *CTFInventoryManager::GetItemInLoadoutForClass( int iClass, int iSlot, CSteamID *pID ) +{ +#ifdef CLIENT_DLL + if ( !pID ) + { + // If they didn't specify a steamID, use the local player + if ( !steamapicontext || !steamapicontext->SteamUser() ) + return NULL; + + CSteamID localSteamID = steamapicontext->SteamUser()->GetSteamID(); + pID = &localSteamID; + } +#endif + + CTFPlayerInventory *pInv = GetInventoryForPlayer( *pID ); + if ( !pInv ) + return GetBaseItemForClass( iClass, iSlot ); + + return pInv->GetItemInLoadout( iClass, iSlot ); +} + +CEconItemView *CTFInventoryManager::GetItemInLoadoutForAccount( int iSlot, CSteamID *pID ) +{ +#ifdef CLIENT_DLL + if ( !pID ) + { + // If they didn't specify a steamID, use the local player + if ( !steamapicontext || !steamapicontext->SteamUser() ) + return NULL; + + CSteamID localSteamID = steamapicontext->SteamUser()->GetSteamID(); + pID = &localSteamID; + } +#endif + + CTFPlayerInventory *pInv = GetInventoryForPlayer( *pID ); + if ( !pInv ) + return NULL; + + return pInv->GetItemInLoadout( GEconItemSchema().GetAccountIndex(), iSlot ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayerInventory *CTFInventoryManager::GetInventoryForPlayer( const CSteamID &playerID ) +{ + for ( int i = 0; i < m_pInventories.Count(); i++ ) + { + if ( m_pInventories[i].pInventory->GetOwner() != playerID ) + continue; + + return assert_cast<CTFPlayerInventory*>( m_pInventories[i].pInventory ); + } + + return NULL; +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFInventoryManager::GetNumItemPickedUpItems( void ) +{ + int iResult = 0; + int iCount = m_LocalInventory.GetItemCount(); + for ( int i = 0; i < iCount; i++ ) + { + if ( IsUnacknowledged( m_LocalInventory.GetItem(i)->GetInventoryPosition() ) && !m_LocalInventory.GetItem(i)->GetStaticData()->IsHidden() ) + { + ++iResult; + } + } + return iResult; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFInventoryManager::ShowItemsPickedUp( bool bForce, bool bReturnToGame, bool bNoPanel ) +{ + // don't show new items in training, unless forced to do so + // i.e. purchased something or traded... + if ( bForce == false && TFGameRules() && ( TFGameRules()->IsInTraining() || TFGameRules()->IsCompetitiveMode() ) ) + { + 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<CEconItemView*> aItemsFound; + + // Go through the root inventory and find any items that are in the "found" position + int iCount = m_LocalInventory.GetItemCount(); + for ( int i = 0; i < iCount; i++ ) + { + CEconItemView *pTmp = m_LocalInventory.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. + NotificationQueue_Remove( &CEconNotification_HasNewItems::IsNotificationType ); + CItemPickupPanel *pItemPanel = bNoPanel ? NULL : EconUI()->OpenItemPickupPanel(); + + if ( pItemPanel ) + { + pItemPanel->SetReturnToGame( bReturnToGame ); + } + + // Only acknowledge items if there is no panel + // Panel will make calls to acknowledge items itself + 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; +} + +//----------------------------------------------------------------------------- +void CTFInventoryManager::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] ); + + if ( iMethod+1 == UNACK_ITEM_PREVIEW_ITEM_PURCHASED ) + { + // If we found a purchased preview item, we want to refresh the store view to remove the discount indicator. + CStorePage* pStorePage = dynamic_cast<CStorePage*>( EconUI()->GetStorePanel()->GetActivePage() ); + if ( pStorePage ) + { + pStorePage->UpdateModelPanels(); + } + } + + // Move it to the first empty backpack position. + if ( bMoveToBackpack ) + { + SetItemBackpackPosition( pItem, 0, false, true ); + } +} + +void CTFInventoryManager::Update( float frametime ) +{ + TM_ZONE_DEFAULT( TELEMETRY_LEVEL0 ); + m_LocalInventory.UpdateWeaponSkinRequest(); + + BaseClass::Update( frametime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFInventoryManager::ShowItemsCrafted( CUtlVector<itemid_t> *vecCraftedIndices ) +{ + CUtlVector<CEconItemView*> aItemsFound; + FOR_EACH_VEC( *vecCraftedIndices, i ) + { + CEconItemView *pItem = m_LocalInventory.GetInventoryItemByItemID( vecCraftedIndices->Element(i) ); + if ( pItem ) + { + // 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( pItem ) ) + { + aItemsFound.AddToTail( pItem ); + } + } + } + + if ( !aItemsFound.Count() ) + return; + + NotificationQueue_Remove( &CEconNotification_HasNewItems::IsNotificationType ); + CItemPickupPanel *pItemPanel = EconUI()->OpenItemPickupPanel(); + for ( int i = 0; i < aItemsFound.Count(); i++ ) + { + pItemPanel->AddItem( aItemsFound[i] ); + + SetAckedByClient( aItemsFound[i] ); + +#ifdef CLIENT_DLL + EconUI()->Gamestats_ItemTransaction( IE_ITEM_RECEIVED, aItemsFound[i], "crafted" ); +#endif + + // Then move it to the first empty backpack position + SetItemBackpackPosition( aItemsFound[i], 0, false, true ); + } + SaveAckFile(); + pItemPanel->MoveToFront(); + + aItemsFound.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFInventoryManager::CheckForRoomAndForceDiscard( void ) +{ + // 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 = m_LocalInventory.GetMaxItemCount(); + int iCount = m_LocalInventory.GetItemCount(); + for ( int i = 0; i < iCount; i++ ) + { + CEconItemView *pTmp = m_LocalInventory.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; +} +#endif + +bool CTFInventoryManager::SlotContainsBaseItems( EEquipType_t eType, int iSlot ) +{ + Assert( eType != EEquipType_t::EQUIP_TYPE_INVALID ); + + if ( eType == EEquipType_t::EQUIP_TYPE_ACCOUNT ) + { + return !IsQuestSlot( iSlot ); + } + + // Passtime gun + if ( (iSlot == LOADOUT_POSITION_UTILITY) && TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + return true; + } + + // Halloween spellbook + if ( iSlot == LOADOUT_POSITION_ACTION ) + { + if ( TFGameRules() && TFGameRules()->IsUsingSpells() ) + return true; + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() ) + return true; + } +#ifdef STAGING_ONLY + return ( ( iSlot < LOADOUT_POSITION_HEAD && iSlot != LOADOUT_POSITION_UTILITY && !IsTauntSlot( iSlot ) ) // Allow utility slots to be empty + || iSlot == LOADOUT_POSITION_PDA3 ); +#else // STAGING_ONLY + // Normal game + return iSlot < LOADOUT_POSITION_HEAD; +#endif // #else +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the item data for the base item in the loadout slot for a given class +//----------------------------------------------------------------------------- +CEconItemView *CTFInventoryManager::GetBaseItemForClass( int iClass, int iSlot ) +{ + // There is no base account item + if ( iClass == GEconItemSchema().GetAccountIndex() ) + return m_pDefaultItem; + + if ( !HushAsserts() ) + { + AssertMsg( iClass >= TF_FIRST_NORMAL_CLASS && iClass < TF_CLASS_COUNT, "Invalid TF_CLASS_: %d", iClass ); + } + Assert( iSlot >= 0 && iSlot < CLASS_LOADOUT_POSITION_COUNT ); + + if ( iClass < TF_FIRST_NORMAL_CLASS || iClass >= TF_CLASS_COUNT || iSlot < 0 || iSlot >= CLASS_LOADOUT_POSITION_COUNT ) + return m_pDefaultItem; + + // Halloween spellbook + if ( iSlot == LOADOUT_POSITION_ACTION ) + { + CUtlVector< item_definition_index_t > stockActionItemDefIndices; + + static CSchemaItemDefHandle pItemDef_SpellBook( "TF_WEAPON_SPELLBOOK" ); + if ( TFGameRules() && TFGameRules()->IsUsingSpells() && pItemDef_SpellBook ) + { + stockActionItemDefIndices.AddToTail( pItemDef_SpellBook->GetDefinitionIndex() ); + } + + static CSchemaItemDefHandle pItemDef_GrapplingHook( "TF_WEAPON_GRAPPLINGHOOK" ); + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() && pItemDef_GrapplingHook ) + { + stockActionItemDefIndices.AddToTail( pItemDef_GrapplingHook->GetDefinitionIndex() ); + } + + static CSchemaItemDefHandle pItemDef_MvMCanteen( "Default Power Up Canteen (MvM)" ); + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && pItemDef_MvMCanteen ) + { + stockActionItemDefIndices.AddToTail( pItemDef_MvMCanteen->GetDefinitionIndex() ); + } + + // Traverse List + for ( CEconItemView *pActionItem : m_pBaseLoadoutItems ) + { + for ( item_definition_index_t defIndex : stockActionItemDefIndices ) + { + if ( pActionItem->GetItemDefIndex() == defIndex ) + return pActionItem; + } + } + } + + if ( iSlot >= LOADOUT_POSITION_HEAD ) + return m_pDefaultItem; + + // Traverse List + FOR_EACH_VEC( m_pBaseLoadoutItems, iItem ) + { + if ( m_pBaseLoadoutItems[iItem]->GetItemDefinition()->GetLoadoutSlot( iClass ) == iSlot ) + return m_pBaseLoadoutItems[iItem]; + } + + return m_pDefaultItem; +} + +//----------------------------------------------------------------------------- +// Purpose: Fills out the vector with the sets that are currently active on the specified player & class +//----------------------------------------------------------------------------- +void CTFInventoryManager::GetActiveSets( CUtlVector<const CEconItemSetDefinition *> *pItemSets, CSteamID steamIDForPlayer, int iClass ) +{ + pItemSets->Purge(); + + CEconItemSchema *pSchema = ItemSystem()->GetItemSchema(); + if ( !pSchema ) + return; + + // Loop through all the items we have equipped, and build a list of only set items + // Accumulate a list of our equipped set items. + CUtlVector<CEconItemView*> equippedSetItems; + for ( int i = 0; i < CLASS_LOADOUT_POSITION_COUNT; i++ ) + { + CEconItemView *pItem = NULL; + +#ifdef GAME_DLL + // On the server we need to look at what the player actually has equipped + // because they might be on a tournament server that's using an item_whitelist + CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( steamIDForPlayer ) ); + if ( pPlayer && pPlayer->Inventory() ) + { + pItem = pPlayer->GetEquippedItemForLoadoutSlot( i ); + } +#else + pItem = TFInventoryManager()->GetItemInLoadoutForClass( iClass, i, &steamIDForPlayer ); +#endif + + if ( !pItem ) + continue; + + CEconItemDefinition* pData = pItem->GetStaticData(); + if ( !pData ) + continue; + + // Ignore items that don't have set bonuses. + if ( !pData->GetItemSetDefinition() ) + continue; + + // Make sure this item isn't failing a Holiday restriction before giving out the set bonus! + if ( pData->GetHolidayRestriction() ) + { + int iHolidayRestriction = UTIL_GetHolidayForString( pData->GetHolidayRestriction() ); + if ( iHolidayRestriction != kHoliday_None && (!TFGameRules() || !TFGameRules()->IsHolidayActive( iHolidayRestriction )) ) + continue; + } + + equippedSetItems.AddToTail( pItem ); + } + + // Find out which sets to apply. + CUtlVector<const char*> testedSets; + for ( int inv = 0; inv < equippedSetItems.Count(); inv++ ) + { + CEconItemView *pItem = equippedSetItems[inv]; + if ( !pItem ) + continue; + + const CEconItemSetDefinition *pItemSet = pItem->GetStaticData()->GetItemSetDefinition(); + if ( !pItemSet ) + continue; + + if ( testedSets.HasElement( pItemSet->m_pszName ) ) + continue; // Don't try to apply set bonuses we have already tested. + + testedSets.AddToTail( pItemSet->m_pszName ); + + // Count how much of this set we have equipped. + int iSetItemsEquipped = 0; + for ( int i=0; i<pItemSet->m_iItemDefs.Count(); i++ ) + { + unsigned int iIndex = pItemSet->m_iItemDefs[i]; + + for ( int j=0; j<equippedSetItems.Count(); j++ ) + { + const CEconItemView *pTestItem = equippedSetItems[j]; + const item_definition_index_t unEquippedItemSetDefIndex = pTestItem->GetItemDefinition()->GetSetItemRemap(); + + if ( iIndex == unEquippedItemSetDefIndex ) + { + iSetItemsEquipped++; + break; + } + } + } + + // Note: this logic will break if we ever have sets that have misc-slot items where we can have multiple items + // of the same type with different remaps equipped. + if ( iSetItemsEquipped == pItemSet->m_iItemDefs.Count() ) + { + // The entire set is equipped. + pItemSets->AddToTail( pItemSet ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: We're generating a base item. We need to add the game-specific keys to the criteria so that it'll find the right base item. +//----------------------------------------------------------------------------- +void CTFInventoryManager::AddBaseItemCriteria( baseitemcriteria_t *pCriteria, CItemSelectionCriteria *pSelectionCriteria ) +{ + pSelectionCriteria->BAddCondition( "used_by_classes", k_EOperator_Subkey_Contains, ItemSystem()->GetItemSchema()->GetClassUsabilityStrings()[pCriteria->iClass], true ); + pSelectionCriteria->BAddCondition( "item_slot", k_EOperator_String_EQ, ItemSystem()->GetItemSchema()->GetLoadoutStrings( EEquipType_t::EQUIP_TYPE_CLASS )[pCriteria->iSlot], true ); +} + + +//======================================================================================================================= +// TF PLAYER INVENTORY +//======================================================================================================================= +// Inventory Less function. +// Used to sort the inventory items into their positions. +class CTFInventoryListLess +{ +public: + bool Less( const CEconItemView &src1, const CEconItemView &src2, void *pCtx ) + { + if ( src1.GetInventoryPosition() > src2.GetInventoryPosition() ) + return true; + + return false; + } +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayerInventory::CTFPlayerInventory() +{ + m_aInventoryItems.SetLessContext( this ); +#ifdef CLIENT_DLL + for ( int i = 0; i < TF_TEAM_COUNT; ++i ) + m_CachedBaseTextureLowRes[ i ].SetLessFunc( DefLessFunc( itemid_t ) ); +#endif + + memset( m_LoadoutItems, LOADOUT_SLOT_USE_BASE_ITEM, sizeof( m_LoadoutItems ) ); + memset( m_AccountLoadoutItems, LOADOUT_SLOT_USE_BASE_ITEM, sizeof( m_AccountLoadoutItems ) ); + ClearClassLoadoutChangeTracking(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayerInventory::~CTFPlayerInventory() +{ +#ifdef CLIENT_DLL + for ( int iTeam = 0; iTeam < TF_TEAM_COUNT; ++iTeam ) + { + FOR_EACH_MAP_FAST( m_CachedBaseTextureLowRes[ iTeam ], i ) + m_CachedBaseTextureLowRes[ iTeam ][ i ]->Release(); + m_CachedBaseTextureLowRes[ iTeam ].RemoveAll(); + } +#endif +} + + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::CheckSaxtonMaskAchievement( const CEconItem *pEconItem ) +{ + if ( pEconItem ) + { + if ( pEconItem->GetDefinitionIndex() == 277 && pEconItem->GetOrigin() == kEconItemOrigin_Crafted ) // Saxton mask is item index 277 + { + g_AchievementMgrTF.OnAchievementEvent( ACHIEVEMENT_TF_HALLOWEEN_CRAFT_SAXTON_MASK ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::UpdateCachedServerLoadoutItems() +{ + V_memcpy( m_CachedServerLoadoutItems, m_LoadoutItems, sizeof( itemid_t ) * ARRAYSIZE( m_CachedServerLoadoutItems ) * ARRAYSIZE( m_CachedServerLoadoutItems[0] ) ); +} +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) +{ + BaseClass::SOUpdated( steamIDOwner, pObject, eEvent ); + +#ifdef CLIENT_DLL + if ( pObject->GetTypeID() != CEconItem::k_nTypeID ) + return; + + // Clear out any predicted backpack slots when items move into them + CEconItem *pEconItem = (CEconItem *)pObject; + if ( eEvent == eSOCacheEvent_Incremental ) + { + EconUI()->GetBackpackPanel()->MarkItemIDDirty( pEconItem->GetItemID() ); + } + int iBackpackPos = TFInventoryManager()->GetBackpackPositionFromBackend( pEconItem->GetInventoryToken() ); + TFInventoryManager()->PredictedBackpackPosFilled( iBackpackPos ); +#endif // CLIENT_DLL +} + +void CTFPlayerInventory::OnHasNewItems() +{ + BaseClass::OnHasNewItems(); +#ifdef CLIENT_DLL + if ( TFGameRules() && TFGameRules()->IsInTraining() ) + return; + + NotificationQueue_Remove( &CEconNotification_HasNewItems::IsNotificationType ); + CEconNotification_HasNewItems *pNotification = new CEconNotification_HasNewItems(); + pNotification->SetText( "TF_HasNewItems" ); + pNotification->SetLifetime( 7.0f ); + NotificationQueue_Add( pNotification ); +#endif +} + +void CTFPlayerInventory::OnHasNewQuest() +{ +#ifdef CLIENT_DLL + +#endif +} + +#ifdef _DEBUG +#ifdef CLIENT_DLL +CON_COMMAND( cl_newitem_test, "Tests the new item ui notification." ) +{ + if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) + return; + + CEconNotification_HasNewItems *pNotification = new CEconNotification_HasNewItems(); + pNotification->SetText( "TF_HasNewItems" ); + pNotification->SetLifetime( 7.0f ); + NotificationQueue_Add( pNotification ); +} +#endif +#endif // _DEBUG + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::ValidateInventoryPositions( void ) +{ + BaseClass::ValidateInventoryPositions(); + +#ifdef CLIENT_DLL + bool bHasNewItems = false; + const int iMaxItems = GetMaxItemCount(); + // First, check for duplicate positions + int iCount = m_aInventoryItems.Count(); + for ( int i = iCount-1; i >= 0; i-- ) + { + CEconItemView *pEconItemView = &m_aInventoryItems[i]; + + CheckSaxtonMaskAchievement( pEconItemView->GetSOCData() ); + + uint32 iPosition = pEconItemView->GetInventoryPosition(); + + // Waiting to be acknowledged? + if ( IsUnacknowledged(iPosition) ) + { + if ( !pEconItemView->GetStaticData()->IsHidden() ) + { + bHasNewItems = true; + } + continue; + } + + bool bInvalidSlot = false; + if ( !IsNewPositionFormat(iPosition) ) + { + ConvertOldFormatInventoryToNew(); + break; + } + + // Inside the backpack? + if ( i < (iCount-1) ) + { + // We're not in an invalid slot yet. But if we're in the same position as another item, we should be moved too. + int iPos1 = TFInventoryManager()->GetBackpackPositionFromBackend(iPosition); + int iPos2 = TFInventoryManager()->GetBackpackPositionFromBackend(m_aInventoryItems[i+1].GetInventoryPosition()); + if ( iPos1 == iPos2 ) + { + Warning("WARNING: Found item in a duplicate backpack position. Moving to the backpack end.\n" ); + bInvalidSlot = true; + } + } + + // Make sure it's not outside the backpack extents + if ( !bInvalidSlot ) + { + bInvalidSlot = (TFInventoryManager()->GetBackpackPositionFromBackend(iPosition) > iMaxItems); + } + + if ( bInvalidSlot ) + { + // The item is NOT hidden and is in an invalid slot. Move it back to the backpack. + if ( pEconItemView->GetItemDefinition() && !pEconItemView->GetItemDefinition()->IsHidden() && !TFInventoryManager()->SetItemBackpackPosition( pEconItemView, 0, true ) ) + { + // We failed to move it to the backpack, because the player has no room. + // Force them to "refind" the item, which will make them throw something out. + TFInventoryManager()->UpdateInventoryPosition( this, pEconItemView->GetItemID(), GetUnacknowledgedPositionFor(UNACK_ITEM_DROPPED) ); + } + } + + // Make sure it isn't equipped by any invalid classes. + for ( int j = TF_FIRST_NORMAL_CLASS; j <= TF_LAST_NORMAL_CLASS; j++ ) + { + if ( !pEconItemView->GetStaticData()->CanBeUsedByClass( j ) && + pEconItemView->IsEquippedForClass( j ) ) + { + // Unequip this item from this class. + InventoryManager()->UpdateInventoryEquippedState( this, INVALID_ITEM_ID, j, pEconItemView->GetEquippedPositionForClass( j ) ); + } + } + } + + if ( bHasNewItems ) + { + OnHasNewItems(); + } +#endif +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::ConvertOldFormatInventoryToNew( void ) +{ + uint32 iBackpackPos = 1; + + // Loop through all items in the inventory. Move them all to the backpack, and in order. + int iCount = m_aInventoryItems.Count(); + for ( int i = 0; i < iCount; i++ ) + { + uint32 iPosition = m_aInventoryItems[i].GetInventoryPosition(); + + // Waiting to be acknowledged? + if ( IsUnacknowledged(iPosition) ) + continue; + + TFInventoryManager()->SetItemBackpackPosition( &m_aInventoryItems[i], iBackpackPos, true ); + iBackpackPos++; + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Creates a script item and associates it with this econ item +//----------------------------------------------------------------------------- +void CTFPlayerInventory::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) +{ + BaseClass::SOCreated( steamIDOwner, pObject, eEvent ); + + if ( pObject->GetTypeID() != CEconItem::k_nTypeID ) + return; + + CEconItem *pEconItem = (CEconItem *)pObject; + // CEconItem *pEconItem = assert_cast<CEconItem*>( pObject ); + +#ifdef CLIENT_DLL + if ( InventoryManager()->GetLocalInventory() == this && GetOwner() == steamIDOwner ) + { + if ( pObject->GetTypeID() == CEconItem::k_nTypeID ) + { + CheckSaxtonMaskAchievement( (CEconItem*)pObject ); + } + } + +// CSteamID ownerSteamID( pEconItem->GetAccountID(), GetUniverse(), k_EAccountTypeIndividual ); +// if ( ownerSteamID == ClientSteamContext().GetLocalPlayerSteamID() ) +// { +// CheckSaxtonMaskAchievement( pEconItem ); +// } + + static CSchemaItemDefHandle pItemDef_HardyLaurel( "The Hardy Laurel" ); + if ( pEconItem->GetItemDefinition() == pItemDef_HardyLaurel ) + { + if ( TFSharedContentManager() ) + { + TFSharedContentManager()->OfferSharedVision( TF_VISION_FILTER_ROME, pEconItem->GetAccountID() ); + } + } + #else + // Summer 2015 Operation Pass so players can display the coin + //tagES remove this when we have a coin equip slot + static CSchemaItemDefHandle pItemDef_Summer2015Operation( "Activated Summer 2015 Operation Pass" ); + if ( pEconItem->GetItemDefinition() == pItemDef_Summer2015Operation ) + { + CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( GetOwner() ) ); + if ( pPlayer ) + { + pPlayer->SetCampaignMedalActive( CAMPAIGN_MEDAL_SUMMER2015 ); + } + } + + // Invasion Community Update Pass so players can display the coin + //tagES remove this when we have a coin equip slot + static CSchemaItemDefHandle pItemDef_InvasionPass( "Activated Invasion Pass" ); + if ( pEconItem->GetItemDefinition() == pItemDef_InvasionPass ) + { + CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( GetOwner() ) ); + if ( pPlayer ) + { + pPlayer->SetCampaignMedalActive( CAMPAIGN_MEDAL_INVASION ); + } + } + + // Halloween Pass so players can display the coin + //tagES remove this when we have a coin equip slot + static CSchemaItemDefHandle pItemDef_HalloweenPass( "Activated Halloween Pass" ); + if ( pEconItem->GetItemDefinition() == pItemDef_HalloweenPass ) + { + CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( GetOwner() ) ); + if ( pPlayer ) + { + pPlayer->SetCampaignMedalActive( CAMPAIGN_MEDAL_HALLOWEEN ); + } + } + + // Winter2016 Pass so players can display the stamp + //tagES remove this when we have a coin equip slot + static CSchemaItemDefHandle pItemDef_Winter2016Pass( "Activated Operation Tough Break Pass" ); + if ( pEconItem->GetItemDefinition() == pItemDef_Winter2016Pass ) + { + CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( GetOwner() ) ); + if ( pPlayer ) + { + pPlayer->SetCampaignMedalActive( CAMPAIGN_MEDAL_WINTER2016 ); + } + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::DumpInventoryToConsole( bool bRoot ) +{ + if ( bRoot ) + { + Msg("========================================\n"); +#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) at backpack slot %d\n", m_aInventoryItems[i].GetStaticData()->GetDefinitionName(), m_aInventoryItems[i].GetItemID(), TFInventoryManager()->GetBackpackPositionFromBackend( m_aInventoryItems[i].GetInventoryPosition() ) ); + + int iEquipped = 0; + for ( equipped_class_t eq = TF_FIRST_NORMAL_CLASS; eq < TF_LAST_NORMAL_CLASS; eq++ ) + { + if ( m_aInventoryItems[i].IsEquippedForClass( eq ) ) + { + if ( iEquipped == 0 ) + { + iEquipped++; + Msg(" -> EQUIPPED: "); + } + Msg("%s ", g_aPlayerClassNames_NonLocalized[eq] ); + } + } + + if ( iEquipped ) + { + Msg("\n"); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::ClearClassLoadoutChangeTracking( void ) +{ + memset(m_bLoadoutChanged,0, sizeof(m_bLoadoutChanged)); +#ifdef CLIENT_DLL + UpdateCachedServerLoadoutItems(); +#endif // CLIENT_DLL +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: Find the low res +//----------------------------------------------------------------------------- +ITexture *CTFPlayerInventory::GetWeaponSkinBaseLowRes( itemid_t nItemId, int iTeam ) const +{ + Assert( iTeam >= 0 && iTeam < TF_TEAM_COUNT ); + int index = m_CachedBaseTextureLowRes[ iTeam ].Find( nItemId ); + if ( index == m_CachedBaseTextureLowRes[ iTeam ].InvalidIndex() ) + return NULL; + + return m_CachedBaseTextureLowRes[ iTeam ][ index ]; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Find the first item in the local user's inventory with the given def index +//----------------------------------------------------------------------------- +CEconItemView *CTFPlayerInventory::GetFirstItemOfItemDef( item_definition_index_t nDefIndex, CPlayerInventory* pInventory ) +{ +#ifdef CLIENT_DLL + if ( pInventory == NULL ) + { + pInventory = InventoryManager()->GetLocalInventory(); + } +#endif + + if ( !pInventory ) + return NULL; + + for ( int i = 0; i < pInventory->GetItemCount(); i++ ) + { + CEconItemView *pItem = pInventory->GetItem(i); + if ( pItem->GetItemDefIndex() == nDefIndex ) + { + return pItem; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the item in the specified loadout slot for a given class +//----------------------------------------------------------------------------- +CEconItemView *CTFPlayerInventory::GetItemInLoadout( int iClass, int iSlot ) +{ + if ( iSlot < 0 || iSlot >= CLASS_LOADOUT_POSITION_COUNT ) + return NULL; + + if ( iClass == GEconItemSchema().GetAccountIndex() ) + { + return GetInventoryItemByItemID( m_AccountLoadoutItems[ iSlot ] ); + } + else + { + if ( iClass < TF_FIRST_NORMAL_CLASS || iClass >= TF_LAST_NORMAL_CLASS ) + return NULL; + + // If we don't have an item in the loadout at that slot, we return the base item + if ( m_LoadoutItems[iClass][iSlot] != LOADOUT_SLOT_USE_BASE_ITEM ) + { + CEconItemView *pItem = GetInventoryItemByItemID( m_LoadoutItems[iClass][iSlot] ); + + // To protect against users lying to the backend about the position of their items, + // we need to validate their position on the server when we retrieve them. + if ( pItem && AreSlotsConsideredIdentical( pItem->GetStaticData()->GetEquipType(), pItem->GetStaticData()->GetLoadoutSlot( iClass ), iSlot ) ) + return pItem; + } + } + + return TFInventoryManager()->GetBaseItemForClass( iClass, iSlot ); +} + +#ifdef CLIENT_DLL +CEconItemView *CTFPlayerInventory::GetCacheServerItemInLoadout( int iClass, int iSlot ) +{ + if ( iSlot < 0 || iSlot >= CLASS_LOADOUT_POSITION_COUNT ) + return NULL; + if ( iClass < TF_FIRST_NORMAL_CLASS || iClass >= TF_LAST_NORMAL_CLASS ) + return NULL; + + // If we don't have an item in the loadout at that slot, we return the base item + if ( m_CachedServerLoadoutItems[iClass][iSlot] != LOADOUT_SLOT_USE_BASE_ITEM ) + { + CEconItemView *pItem = GetInventoryItemByItemID( m_CachedServerLoadoutItems[iClass][iSlot] ); + + // To protect against users lying to the backend about the position of their items, + // we need to validate their position on the server when we retrieve them. + if ( pItem && AreSlotsConsideredIdentical( pItem->GetStaticData()->GetEquipType(), pItem->GetStaticData()->GetLoadoutSlot( iClass ), iSlot ) ) + return pItem; + } + + return TFInventoryManager()->GetBaseItemForClass( iClass, iSlot ); +} +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static CEconGameAccountClient *GetSOCacheGameAccountClient( CGCClientSharedObjectCache *pSOCache ) +{ + if ( !pSOCache ) + return NULL; + + return pSOCache->GetSingleton<CEconGameAccountClient>(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayerInventory::GetPreviewItemDef( void ) const +{ + CEconGameAccountClient *pGameAccountClient = GetSOCacheGameAccountClient( m_pSOCache ); + if ( !pGameAccountClient ) + return 0; + + return pGameAccountClient->Obj().preview_item_def(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayerInventory::CanPurchaseItems( int iItemCount ) const +{ + // If we're not a free trial account, we fall back to our default logic of "do + // we have enough empty slots?". + CEconGameAccountClient *pGameAccountClient = GetSOCacheGameAccountClient( m_pSOCache ); + if ( !pGameAccountClient || !pGameAccountClient->Obj().trial_account() ) + return BaseClass::CanPurchaseItems( iItemCount ); + + // We're a free trial account, so when we purchase these items, our inventory + // will actually expand. We check to make sure that we have room for these + // items against what will be our new maximum backpack size, not our current + // backpack limit. + int iNewItemCount = GetItemCount() + iItemCount, + iAfterPurchaseMaxItemCount = DEFAULT_NUM_BACKPACK_SLOTS + + (pGameAccountClient ? pGameAccountClient->Obj().additional_backpack_slots() : 0); + + return iNewItemCount <= iAfterPurchaseMaxItemCount; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayerInventory::GetMaxItemCount( void ) const +{ + int iMaxItems = DEFAULT_NUM_BACKPACK_SLOTS; + CEconGameAccountClient *pGameAccountClient = GetSOCacheGameAccountClient( m_pSOCache ); + if ( pGameAccountClient ) + { + if ( pGameAccountClient->Obj().trial_account() ) + { + iMaxItems = DEFAULT_NUM_BACKPACK_SLOTS_FREE_TRIAL_ACCOUNT; + } + iMaxItems += pGameAccountClient->Obj().additional_backpack_slots(); + } + return MIN( iMaxItems, MAX_NUM_BACKPACK_SLOTS ); +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: Removes any item in a loadout slot. If the slot has a base item, +// the player essentially returns to using that item. +//----------------------------------------------------------------------------- +bool CTFPlayerInventory::ClearLoadoutSlot( int iClass, int iSlot ) +{ + if ( iSlot < 0 || iSlot >= CLASS_LOADOUT_POSITION_COUNT ) + return false; + + if ( iClass == GEconItemSchema().GetAccountIndex() ) + { + if ( m_AccountLoadoutItems[iSlot] == LOADOUT_SLOT_USE_BASE_ITEM ) + return false; + } + else + { + if ( iClass < TF_FIRST_NORMAL_CLASS || iClass >= TF_LAST_NORMAL_CLASS ) + return false; + + if ( m_LoadoutItems[iClass][iSlot] == LOADOUT_SLOT_USE_BASE_ITEM ) + return false; + } + + CEconItemView *pItemInSlot = GetItemInLoadout( iClass, iSlot ); + if ( !pItemInSlot ) + return false; + + InventoryManager()->UpdateInventoryEquippedState( this, INVALID_ITEM_ID, iClass, iSlot ); + + // TODO: Prediction + // It's been moved to the backpack, so clear out loadout entry + //m_LoadoutItems[iClass][iSlot] = LOADOUT_SLOT_USE_BASE_ITEM; + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Update weapon skin request list +//----------------------------------------------------------------------------- +void CTFPlayerInventory::UpdateWeaponSkinRequest() +{ + TM_ZONE_DEFAULT( TELEMETRY_LEVEL0 ); + + FOR_EACH_VEC_BACK( m_vecWeaponSkinRequestList, i ) + { + SkinRequest_t &req = m_vecWeaponSkinRequestList[i]; + CEconItemView *pItem = GetInventoryItemByItemID( req.m_nID ); + Assert( pItem ); + if ( !pItem ) + { + m_vecWeaponSkinRequestList.Remove( i ); + continue; + } + + // Have we loaded this one yet? If not, do it here. + if ( mdlcache->IsErrorModel( req.m_hModel ) ) + { + const char *pszModelName = pItem->GetPlayerDisplayModel( 0, 0 ); + if ( pszModelName ) + { + MDLHandle_t hItemMDL = mdlcache->FindMDL( pszModelName ); + if ( mdlcache->IsErrorModel( hItemMDL ) ) + { + AssertMsg( 0, "failed to find %s from mdlcache", pszModelName ); + m_vecWeaponSkinRequestList.Remove( i ); + continue; + } + + req.m_hModel = hItemMDL; + } + else + { + AssertMsg( 0, "failed to precache weapon skin because there's no model" ); + m_vecWeaponSkinRequestList.Remove( i ); + } + } + + // Draw! + { + CMDL mdl; + mdl.SetMDL( req.m_hModel ); + mdl.m_pProxyData = static_cast<IClientRenderable*>( pItem ); + + pItem->SetWeaponSkinUseLowRes( true ); + int nRestoreTeam = pItem->GetTeamNumber(); + pItem->SetTeamNumber( req.m_nTeam ); + + matrix3x4_t matIdentity; + SetIdentityMatrix( matIdentity ); + IMaterial *pOverrideMaterial = pItem->GetMaterialOverride( pItem->GetTeamNumber() ); + + if ( pOverrideMaterial ) + modelrender->ForcedMaterialOverride( pOverrideMaterial ); + + mdl.Draw( matIdentity ); + + if ( pOverrideMaterial ) + modelrender->ForcedMaterialOverride( NULL ); + + pItem->SetTeamNumber( nRestoreTeam ); + pItem->SetWeaponSkinUseLowRes( false ); + } + + // Don't remove until it's complete. + if ( pItem->GetWeaponSkinBase() ) + { + Assert( pItem->GetWeaponSkinBaseCompositor() == NULL ); + ITexture* pTex = pItem->GetWeaponSkinBase(); + if ( pTex ) + { + pTex->AddRef(); // We need to hold a ref to the texture. + m_CachedBaseTextureLowRes[ req.m_nTeam ].Insert( req.m_nID, pTex ); + pItem->SetWeaponSkinBase( NULL ); // Clean up. + } + + // We do RED, then BLUE. Then drop it out of the list. + if ( req.m_nTeam == TF_TEAM_BLUE ) + { + Assert( !mdlcache->IsErrorModel( req.m_hModel ) ); + mdlcache->Release( req.m_hModel ); + m_vecWeaponSkinRequestList.Remove( i ); + } + else + { + Assert( req.m_nTeam == TF_TEAM_RED ); + req.m_nTeam = TF_TEAM_BLUE; + } + } + + // Only do one of these per frame to avoid hitching. + break; + } +} + +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::ItemHasBeenUpdated( CEconItemView *pItem, bool bUpdateAckFile, bool bWriteAckFile ) +{ + Assert( pItem ); + + BaseClass::ItemHasBeenUpdated( pItem, bUpdateAckFile, bWriteAckFile ); + const CEconItem *pEconItem = pItem->GetSOCData(); + +#ifdef CLIENT_DLL + static CSchemaAttributeDefHandle pAttrib_WeaponAllowInspect( "weapon_allow_inspect" ); + bool bLocalInv = InventoryManager()->GetLocalInventory() == this; +#endif // CLIENT_DLL + + for ( equipped_class_t iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ ) + { + equipped_slot_t unSlot = pEconItem ? pEconItem->GetEquippedPositionForClass( iClass ) : INVALID_EQUIPPED_SLOT; + + Assert( GetInventoryItemByItemID( pItem->GetItemID() ) ); + + bool bThisClassLoadoutChanged = UpdateEquipStateForClass( pItem->GetItemID(), unSlot, m_LoadoutItems[iClass], ARRAYSIZE( m_LoadoutItems[iClass] ) ); + + if ( bThisClassLoadoutChanged ) + { + m_bLoadoutChanged[iClass] = true; + +#ifdef CLIENT_DLL + // if we can inspect this item, it has unique skin. + // draw it once per team to tell the system to generate unique skin + float flInspect = 0; + if ( bLocalInv && FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItem, pAttrib_WeaponAllowInspect, &flInspect ) ) + { + SkinRequest_t req = { TF_TEAM_RED, pItem->GetItemID(), MDLHANDLE_INVALID }; + m_vecWeaponSkinRequestList.AddToTail( req ); + } +#endif // CLIENT_DLL + } + } + + // Update account items as well + equipped_slot_t unSlot = pEconItem ? pEconItem->GetEquippedPositionForClass( GEconItemSchema().GetAccountIndex() ) : INVALID_EQUIPPED_SLOT; + UpdateEquipStateForClass( pItem->GetItemID(), unSlot, m_AccountLoadoutItems, ARRAYSIZE( m_AccountLoadoutItems ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayerInventory::UpdateEquipStateForClass( const itemid_t& itemID, equipped_slot_t nSlot, itemid_t *pLoadout, int nCount ) +{ + bool bThisClassLoadoutChanged = false; + + // For each slot this item may have been equipped in, set to the base item, unless that slot + // is the current slot. (ie., if an item moves from slot 10 to slot 12, slot 10 will be set to + // base item, and slot 12 will be set to this item ID) + for ( int i = 0; i < nCount; i++ ) + { + itemid_t& refLoadoutItemID = *( pLoadout + i ); + + if ( i == nSlot ) + { + // This may be an invalid slot for the item, but CTFPlayerInventory::InventoryReceived() + // will have detected that and sent off a request already to move it. The response + // to that will clear this loadout slot. + refLoadoutItemID = itemID; + bThisClassLoadoutChanged = true; + } + else if ( refLoadoutItemID == itemID ) + { + refLoadoutItemID = LOADOUT_SLOT_USE_BASE_ITEM; + bThisClassLoadoutChanged = true; + } + } + + return bThisClassLoadoutChanged; +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::PostSOUpdate( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) +{ + BaseClass::PostSOUpdate( steamIDOwner, eEvent ); + + VerifyChangedLoadoutsAreValid(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) +{ + BaseClass::SOCacheSubscribed( steamIDOwner, eEvent ); + + VerifyChangedLoadoutsAreValid(); + UpdateCachedServerLoadoutItems(); +} + +//----------------------------------------------------------------------------- +// Purpose: Helper function to add a new item for a econ item +//----------------------------------------------------------------------------- +bool CTFPlayerInventory::AddEconItem( CEconItem * pItem, bool bUpdateAckFile, bool bWriteAckFile, bool bCheckForNewItems ) +{ + if ( BaseClass::AddEconItem( pItem, bUpdateAckFile, bWriteAckFile, bCheckForNewItems ) ) + { + if ( bCheckForNewItems && InventoryManager()->GetLocalInventory() == this ) + { + if ( IsUnacknowledged( pItem->GetInventoryToken() ) ) + { + OnHasNewQuest(); + } + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::VerifyLoadoutItemsAreValid( int iClass ) +{ + // Important note: currently this function walks the loadout slots in order causing slots towards + // the beginning of the list to take priority over slots later in the list. This means that currently + // weapons take priority over hats, hats take priority over misc slots, etc. which is all well and good. + // If later we want the order in which slots claim their equip regions to change, we'll want to change + // the iteration order here and also change GenerateEquipRegionMaskUpToSlot(), which is used for + // filling out the UI. + equip_region_mask_t unCumulativeRegionMask = 0; + for ( int i = 0; i < CLASS_LOADOUT_POSITION_COUNT; i++ ) + { + CEconItemView *pEquippedItemView = GetItemInLoadout( iClass, i ); + if ( !pEquippedItemView ) + continue; + + // Does this item use the same regions as some item that we already have equipped? + equip_region_mask_t unItemEquipMask = pEquippedItemView->GetItemDefinition()->GetEquipRegionMask(); + if ( unItemEquipMask & unCumulativeRegionMask ) + { + // Unequip this item. This will wind up calling into ::ItemHasBeenUpdated() once the + // unequip makes it to the GC and back. + InventoryManager()->UpdateInventoryEquippedState( this, INVALID_ITEM_ID, iClass, pEquippedItemView->GetEquippedPositionForClass( iClass ) ); + } + else + { + unCumulativeRegionMask |= unItemEquipMask; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::VerifyChangedLoadoutsAreValid() +{ + // We maybe changed equip state for an item and it's possible if we're sending bad messages and/or + // hacking state that we'll now have conflicting items equipped. Walk all of our items for this class + // to verify our state is good. + for ( equipped_class_t iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ ) + { + if ( m_bLoadoutChanged[iClass] ) + { + VerifyLoadoutItemsAreValid( iClass ); + } + } +} +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayerInventory::ItemIsBeingRemoved( CEconItemView *pItem ) +{ + for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ ) + { + if ( pItem->IsEquippedForClass( iClass ) ) + { + for ( int iSlot = 0; iSlot < CLASS_LOADOUT_POSITION_COUNT; iSlot++ ) + { + if ( m_LoadoutItems[iClass][iSlot] == pItem->GetItemID() ) + { + m_LoadoutItems[iClass][iSlot] = LOADOUT_SLOT_USE_BASE_ITEM; + m_bLoadoutChanged[iClass] = true; + } + } + } + } +} + + +//======================================================================================================================= +// TF PLAYER INVENTORY +//======================================================================================================================= + +#ifdef CLIENT_DLL +// WTF: Declaring this inline caused a compiler bug. +CTFPlayerInventory *CTFInventoryManager::GetLocalTFInventory( void ) +{ + return &m_LocalInventory; +} +#endif + +#ifdef _DEBUG +#if defined(CLIENT_DLL) +CON_COMMAND_F( item_dumpinv_other, "Dumps the contents of a specified client inventory. Format: item_dumpinv_other <player index>", FCVAR_NONE ) +{ + if ( args.ArgC() > 1 ) + { + C_TFPlayer *pOther = ToTFPlayer( UTIL_PlayerByIndex( atoi(args[1]) ) ); + if ( pOther ) + { + pOther->Inventory()->DumpInventoryToConsole( true ); + return; + } + } + Msg("Couldn't find specified player.\nFormat: item_dumpinv_other <player index>\n"); +} +#endif +#endif // _DEBUG + +#ifdef _DEBUG +#if defined(CLIENT_DLL) +CON_COMMAND_F( item_dumpinv, "Dumps the contents of a specified client inventory. Format: item_dumpinv", FCVAR_CHEAT ) +#else +CON_COMMAND_F( item_dumpinv_sv, "Dumps the contents of a specified server inventory. Format: item_dumpinv_sv", FCVAR_CHEAT ) +#endif +{ +#if defined(CLIENT_DLL) + CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); +#else + CSteamID steamID; + CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() ); + pPlayer->GetSteamID( &steamID ); +#endif + + CPlayerInventory *pInventory = TFInventoryManager()->GetInventoryForPlayer( steamID ); + if ( !pInventory ) + { + Msg("No inventory for that player.\n"); + return; + } + + pInventory->DumpInventoryToConsole( true ); +} +#endif + +#ifdef _DEBUG +#if defined(CLIENT_DLL) +CON_COMMAND_F( item_deleteunknowns, "Deletes all items in your inventory that we don't have static data for. Useful for removing items that have been removed from the backend.", FCVAR_CHEAT ) +{ + int iDeleted = TFInventoryManager()->DeleteUnknowns( InventoryManager()->GetLocalInventory() ); + Msg("Deleted %d unknown items.\n", iDeleted); +} +#endif +#endif + + +#if defined( TF_CLIENT_DLL ) +CON_COMMAND( load_itempreset, "Equip all items for a given preset on the player." ) +{ + C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( !pPlayer ) + return; + + if ( args.ArgC() != 2 ) + { + Msg( "Usage: \"load_itempreset <preset index>\" - <preset index> can be 0 to 3." ); + return; + } + + if ( pPlayer->m_Shared.InCond( TF_COND_TAUNTING ) ) + return; + + equipped_class_t unClass = pPlayer->GetPlayerClass()->GetClassIndex(); + equipped_preset_t unPreset = atoi( args[1] ); + if ( TFInventoryManager()->LoadPreset( unClass, unPreset ) ) + { + // Tell the GC to tell server that we should respawn if we're in a respawn room + extern ConVar tf_respawn_on_loadoutchanges; + if ( tf_respawn_on_loadoutchanges.GetBool() ) + { + GCSDK::CGCMsg< ::MsgGCEmpty_t > msg( k_EMsgGCRespawnPostLoadoutChange ); + GCClientSystem()->BSendMessage( msg ); + } + } +} +#endif // TF_CLIENT_DLL |