diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/shared/econ/econ_store.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/shared/econ/econ_store.cpp')
| -rw-r--r-- | game/shared/econ/econ_store.cpp | 1672 |
1 files changed, 1672 insertions, 0 deletions
diff --git a/game/shared/econ/econ_store.cpp b/game/shared/econ/econ_store.cpp new file mode 100644 index 0000000..9c69363c --- /dev/null +++ b/game/shared/econ/econ_store.cpp @@ -0,0 +1,1672 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Common objects and utilities related to the in-game item store +// +//============================================================================= + +#include "cbase.h" +#include "econ_store.h" +#include "gcsdk/enumutils.h" + +#if defined(CLIENT_DLL) +#include "econ_ui.h" +#include "store/store_panel.h" +#include "econ_item_inventory.h" +#else +#include "gcsdk/gcconstants.h" +#endif + +#if defined(CLIENT_DLL) || defined(GAME_DLL) +#include "econ_item_system.h" +#endif + +// For localization +#include "tier3/tier3.h" +#include "vgui/ILocalize.h" +#include "tier0/icommandline.h" + +#ifdef CLIENT_DLL +// For formatting in locale +#include <string> +#include <sstream> +#endif // CLIENT_DLL + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar store_version( "store_version", "1", FCVAR_CLIENTDLL | FCVAR_HIDDEN | FCVAR_ARCHIVE, "Which version of the store to display." ); + +ENUMSTRINGS_START( ECurrency ) + { k_ECurrencyUSD, "USD" }, + { k_ECurrencyGBP, "GBP" }, + { k_ECurrencyEUR, "EUR" }, + { k_ECurrencyRUB, "RUB" }, + { k_ECurrencyBRL, "BRL" }, + { k_ECurrencyJPY, "JPY" }, + { k_ECurrencyNOK, "NOK" }, + { k_ECurrencyIDR, "IDR" }, + { k_ECurrencyMYR, "MYR" }, + { k_ECurrencyPHP, "PHP" }, + { k_ECurrencySGD, "SGD" }, + { k_ECurrencyTHB, "THB" }, + { k_ECurrencyVND, "VND" }, + { k_ECurrencyKRW, "KRW" }, + { k_ECurrencyTRY, "TRY" }, + { k_ECurrencyUAH, "UAH" }, + { k_ECurrencyMXN, "MXN" }, + { k_ECurrencyCAD, "CAD" }, + { k_ECurrencyAUD, "AUD" }, + { k_ECurrencyNZD, "NZD" }, + { k_ECurrencyPLN, "PLN" }, + { k_ECurrencyCHF, "CHF" }, + { k_ECurrencyCNY, "CNY" }, + { k_ECurrencyTWD, "TWD" }, + { k_ECurrencyHKD, "HKD" }, + { k_ECurrencyINR, "INR" }, + { k_ECurrencyAED, "AED" }, + { k_ECurrencySAR, "SAR" }, + { k_ECurrencyZAR, "ZAR" }, + { k_ECurrencyCOP, "COP" }, + { k_ECurrencyPEN, "PEN" }, + { k_ECurrencyCLP, "CLP" }, + { k_ECurrencyInvalid, "Invalid" } +ENUMSTRINGS_REVERSE( ECurrency, k_ECurrencyInvalid ) + +ENUMSTRINGS_START( EPurchaseState ) +{ k_EPurchaseStateInvalid, "Invalid" }, +{ k_EPurchaseStateInit, "Init" }, +{ k_EPurchaseStateWaitingForAuthorization, "WaitingForAuthorization" }, +{ k_EPurchaseStatePending, "Pending" }, +{ k_EPurchaseStateComplete, "Complete" }, +{ k_EPurchaseStateFailed, "Failed" }, +{ k_EPurchaseStateCanceled, "Canceled" }, +{ k_EPurchaseStateRefunded, "Refunded" }, +{ k_EPurchaseStateChargeback, "Chargeback" }, +{ k_EPurchaseStateChargebackReversed, "Chargeback Reversed" }, +ENUMSTRINGS_END( EPurchaseState ) + +ENUMSTRINGS_START( EPurchaseResult ) +{ k_EPurchaseResultOK, "OK" }, +{ k_EPurchaseResultFail, "Fail" }, +{ k_EPurchaseResultInvalidParam, "InvalidParam" }, +{ k_EPurchaseResultInternalError, "InternalError" }, +{ k_EPurchaseResultNotApproved, "NotApproved" }, +{ k_EPurchaseResultAlreadyCommitted, "AlreadyCommitted" }, +{ k_EPurchaseResultUserNotLoggedIn, "UserNotLoggedIn" }, +{ k_EPurchaseResultWrongCurrency, "WrongCurrency" }, +{ k_EPurchaseResultAccountError, "AccountError" }, +{ k_EPurchaseResultInsufficientFunds, "InsufficientFunds Reversed" }, +{ k_EPurchaseResultTimedOut, "TimedOut" }, +{ k_EPurchaseResultAcctDisabled, "AcctDisabled" }, +{ k_EPurchaseResultAcctCannotPurchase, "AcctCannotPurchase" }, +{ k_EMicroTxnResultFailedFraudChecks, "PurchaseFailedSupport" }, // this string is mismatched so "fraud" doesn't appear in the client code +{ k_EPurchaseResultOldPriceSheet, "OldPriceSheet" }, +{ k_EPurchaseResultTxnNotFound, "TxnNotFound" }, +ENUMSTRINGS_END( EPurchaseResult ) + +ENUMSTRINGS_START( EGCTransactionAuditReason ) +{ k_EGCTransactionAudit_GCTransactionCompleted, "Completed" }, +{ k_EGCTransactionAudit_GCTransactionInit, "Init" }, +{ k_EGCTransactionAudit_GCTransactionPostInit, "Post Init" }, +{ k_EGCTransactionAudit_GCTransactionFinalize, "Finalize" }, +{ k_EGCTransactionAudit_GCTransactionFinalizeFailed, "Finalize Failed" }, +{ k_EGCTransactionAudit_GCTransactionCanceled, "Canceled" }, +{ k_EGCTransactionAudit_SteamFailedMismatch, "Steam Failed State Mismatch" }, +{ k_EGCTransactionAudit_GCRemovePurchasedItems, "Remove Purchased Items" }, +{ k_EGCTransactionAudit_GCTransactionInsert, "Transaction Insert" }, +{ k_EGCTransactionAudit_GCTransactionCompletedPostChargeback, "Completed (Post-Chargeback)" }, +ENUMSTRINGS_END( EGCTransactionAuditReason ) + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void econ_store_entry_t::InitCategoryTags( const char *pTags ) +{ + // Default to "unrentable". + m_fRentalPriceScale = 1.0f; + + if ( !pTags || !pTags[0] ) + return; + + // Cache tags - this pointer comes out of a KeyValues that isn't deleted + m_pchCategoryTags = pTags; + + // Split the tags apart + CUtlVector< char * > vecTokens; + V_SplitString( pTags, "+", vecTokens ); + + // Sanity check the results of V_SplitString() +#ifdef _DEBUG + const int nTagsLength = V_strlen( pTags ); + int nSepCount = 0; // # of separators + for ( int i = 0; i < nTagsLength; ++i ) + { + if ( pTags[i] == '+' ) + ++nSepCount; + } +// Assert( ( vecTokens.Size() - 1 ) == nSepCount ); +#endif + + // Calculate the maximum that we want to charge for this rental. + float fMaxRentalPriceScale = 0.0f; + + // Generate symbols for each tag + FOR_EACH_VEC( vecTokens, i ) + { + CategoryTag_t info; + info.m_strName = vecTokens[i]; + info.m_unID = CEconStoreCategoryManager::GetCategoryID( vecTokens[i] ); + m_vecCategoryTags.AddToTail( info ); + + const float fCategoryRentalPriceScale = GetEconPriceSheet()->GetRentalPriceScale( vecTokens[i] ); + fMaxRentalPriceScale = MAX( fMaxRentalPriceScale, fCategoryRentalPriceScale ); + } + + m_fRentalPriceScale = fMaxRentalPriceScale <= 0.0f || fMaxRentalPriceScale >= 100.0f + ? 1.0f + : fMaxRentalPriceScale * 0.01f; + + // Clean up + vecTokens.PurgeAndDeleteElements(); +} + +void econ_store_entry_t::SetItemDefinitionIndex( item_definition_index_t usDefIndex ) +{ + AssertMsg( usDefIndex != INVALID_ITEM_DEF_INDEX, "Invalid item definition index!" ); + m_usDefIndex = usDefIndex; +} + +bool econ_store_entry_t::IsListedInCategory( StoreCategoryID_t unID ) const +{ + AssertMsg( unID != CEconStoreCategoryManager::k_CategoryID_Invalid, "Did you mean to search for an invalid symbol?" ); + + FOR_EACH_VEC( m_vecCategoryTags, i ) + { + if ( m_vecCategoryTags[i].m_unID == unID ) + return true; + } + + return false; +} + +bool econ_store_entry_t::IsListedInSubcategories( const CEconStoreCategoryManager::StoreCategory_t &Category ) const +{ + FOR_EACH_VEC( Category.m_vecSubcategories, iSubCategory ) + { + if ( IsListedInCategory( Category.m_vecSubcategories[iSubCategory]->m_unID ) ) + return true; + } + + return false; +} + +bool econ_store_entry_t::IsListedInCategoryOrSubcategories( const CEconStoreCategoryManager::StoreCategory_t &Category ) const +{ + return IsListedInCategory( Category.m_unID ) || + IsListedInSubcategories( Category ); +} + +bool econ_store_entry_t::IsOnSale( ECurrency eCurrency ) const +{ + return GetSalePrice( eCurrency ) > 0 + && GetSalePrice( eCurrency ) < GetBasePrice( eCurrency ); +} + +bool econ_store_entry_t::IsRentable() const +{ + return m_fRentalPriceScale < 1.0f + && m_fRentalPriceScale > 0.0f; +} + +#ifdef CLIENT_DLL +bool econ_store_entry_t::HasDiscount( ECurrency eCurrency, item_price_t *out_punOptionalBasePrice ) const +{ + // Items on sale always report as being discounted. + if ( IsOnSale( eCurrency ) ) + { + if ( out_punOptionalBasePrice ) + { + *out_punOptionalBasePrice = GetBasePrice( eCurrency ); + } + + return true; + } + + // If we're not a bundle we have no other way of being on sale -- abort. + const CEconItemDefinition *pItemDef = GetItemSchema()->GetItemDefinition( GetItemDefinitionIndex() ); + if ( !pItemDef || !pItemDef->IsBundle() ) + return false; + + const bundleinfo_t *pBundleInfo = pItemDef->GetBundleInfo(); + Assert( pBundleInfo ); + + item_price_t unTotalPriceOfBundleItems = 0; + FOR_EACH_VEC( pBundleInfo->vecItemDefs, bundleIdx ) + { + const econ_store_entry_t *pCurEntry = pBundleInfo->vecItemDefs[bundleIdx] + ? GetEconPriceSheet()->GetEntry( pBundleInfo->vecItemDefs[bundleIdx]->GetDefinitionIndex() ) + : NULL; + if ( pCurEntry ) + { + unTotalPriceOfBundleItems += pCurEntry->GetCurrentPrice( eCurrency ); + } + } + + // Did the bundle provide an actual discount? + const item_price_t unBasePrice = GetCurrentPrice( eCurrency ); + if ( unBasePrice < unTotalPriceOfBundleItems ) + { + if ( out_punOptionalBasePrice ) + { + *out_punOptionalBasePrice = unTotalPriceOfBundleItems; + } + + return true; + } + + // No discount. Leave the base price pointer untouched. + return false; +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +StoreCategoryID_t econ_store_entry_t::GetCategoryTagIDFromIndex( uint32 iIndex ) const +{ + if ( !IsValidCategoryTagIndex( iIndex ) ) + return CEconStoreCategoryManager::k_CategoryID_Invalid; + + return m_vecCategoryTags[ iIndex ].m_unID; +} + +item_price_t econ_store_entry_t::GetCurrentPrice( ECurrency eCurrency ) const +{ +#ifdef CLIENT_DLL + if ( m_bIsMarketItem ) + { + const client_market_data_t *pClientMarketData = GetClientMarketData( GetItemDefinitionIndex(), AE_UNIQUE ); + if ( !pClientMarketData ) + return 0; + + return pClientMarketData->m_unLowestPrice; + } +#endif + return IsOnSale( eCurrency ) + ? GetSalePrice( eCurrency ) + : GetBasePrice( eCurrency ); +} + +float econ_store_entry_t::GetRentalPriceScale() const +{ + Assert( IsRentable() ); + + return m_fRentalPriceScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void econ_store_entry_t::ValidatePrice( ECurrency eCurrency, item_price_t unPrice ) +{ + if ( m_bIsMarketItem ) + return; + + // If the price is 0 and this is not a pack item (an item that is not individually for sale, but is sold as part of a pack bundle), post an alert + if ( unPrice == 0 && !m_bIsPackItem ) + { + CFmtStr fmtError( "Warning: Invalid price for item (item def=%i)", GetItemDefinitionIndex() ); +#if defined( GC_DLL ) + GGCGameBase()->PostAlert( GCSDK::k_EAlertTypeReport, true, fmtError.Access() ); +#endif + AssertMsg( false, "%s", fmtError.Access() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CEconStorePriceSheet::CEconStorePriceSheet() + : m_pKVRaw( NULL ) + , m_mapEntries( DefLessFunc( uint16 ) ) + , m_mapRentalPriceScales( DefLessFunc( const char * ) ) + , m_RTimeVersionStamp( 0 ) + , m_pStorePromotionFirstTimePurchaseItem( NULL ) + , m_pStorePromotionFirstTimeWebPurchaseItem( NULL ) + , m_unFeaturedItemIndex( 0 ) + , m_eEconStoreSortType( kEconStoreSortType_Name_AToZ ) +{ + Clear(); + m_FeaturedItems.m_pchName = "featured_items"; + + m_mapCurrencyPricePoints.SetLessFunc( &price_point_map_key_t::Less ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CEconStorePriceSheet::~CEconStorePriceSheet() +{ + Clear(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Clears out the parsed data +//----------------------------------------------------------------------------- +void CEconStorePriceSheet::Clear() +{ + if ( m_pKVRaw ) + { + m_pKVRaw->deleteThis(); + m_pKVRaw = NULL; + } + + m_RTimeVersionStamp = 0; + m_mapEntries.RemoveAll(); + m_FeaturedItems.m_vecEntries.RemoveAll(); + m_flPreviewPeriodDiscount = 0; + + m_mapCurrencyPricePoints.Purge(); + +#ifdef CLIENT_DLL + m_vecFeaturedItems.Purge(); +#endif // CLIENT_DLL + + // Clear categories in category manager + ClearEconStoreCategoryManager(); +} + +//----------------------------------------------------------------------------- +// Purpose: Gets the entry details for a specific item +//----------------------------------------------------------------------------- +const econ_store_entry_t *CEconStorePriceSheet::GetEntry( item_definition_index_t usDefIndex ) const +{ + int iIndex = m_mapEntries.Find( usDefIndex ); + if ( m_mapEntries.IsValidIndex( iIndex ) ) + return &m_mapEntries[iIndex]; + + return NULL; +} + +#ifdef GC_DLL +//----------------------------------------------------------------------------- +// Purpose: Gets the entry details for a specific item and lets us modify the contents (GC-only) +//----------------------------------------------------------------------------- +econ_store_entry_t *CEconStorePriceSheet::GetEntryWriteable( item_definition_index_t unDefIndex ) +{ + int iIndex = m_mapEntries.Find( unDefIndex ); + if ( m_mapEntries.IsValidIndex( iIndex ) ) + return &m_mapEntries[iIndex]; + + return NULL; +} +#endif // GC_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool BInsertSinglePricePoint( CurrencyPricePointMap_t& out_mapPricePoints, const price_point_map_key_t& key, item_price_t unValue ) +{ + if ( out_mapPricePoints.Find( key ) != out_mapPricePoints.InvalidIndex() ) + return false; + + out_mapPricePoints.Insert( key, unValue ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool BInitializeCurrencyPricePoints( CurrencyPricePointMap_t& out_mapPricePoints, KeyValues *pKVPricePoints ) +{ + if ( !pKVPricePoints ) + return false; + + // Zero-price-point is universal. + FOR_EACH_CURRENCY( eCurrency ) + { + const price_point_map_key_t key = { 0, eCurrency }; + if ( !BInsertSinglePricePoint( out_mapPricePoints, key, 0 ) ) + return false; + } + + // Individual price points specified in our store config. + FOR_EACH_TRUE_SUBKEY( pKVPricePoints, pKVUSD ) + { + const item_price_t unUSD = Q_atoi( pKVUSD->GetName() ); + + // USD key, for ease of lookup. + { + const price_point_map_key_t key = { unUSD, k_ECurrencyUSD }; + if ( !BInsertSinglePricePoint( out_mapPricePoints, key, unUSD ) ) + return false; + } + + // Exchange rates. + FOR_EACH_VALUE( pKVUSD, pKVOtherCurrency ) + { + const char *pszCurrencyName = pKVOtherCurrency->GetName(); + const ECurrency eCurrency = ECurrencyFromName( pszCurrencyName ); + + if ( eCurrency == k_ECurrencyInvalid ) + { + // Spew +#ifdef GC_DLL + EmitError( SPEW_GC, "Unknown Currency [%s] found in price sheet. Currency is unsupported!\n", pszCurrencyName ); +#endif + // don't crash, just conintue + continue; + } + + const price_point_map_key_t key = { unUSD, eCurrency }; + if ( !BInsertSinglePricePoint( out_mapPricePoints, key, pKVOtherCurrency->GetInt() ) ) + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Parses the KV version of the price sheet +//----------------------------------------------------------------------------- +bool CEconStorePriceSheet::InitFromKV( KeyValues *pKVRoot ) +{ + Clear(); + m_pKVRaw = pKVRoot->MakeCopy(); + + // Initialize categories - needed to initialize store entries + if ( !GEconStoreCategoryManager()->BInit( this, m_pKVRaw ) ) + { +#ifdef GC_DLL + EmitError( SPEW_GC, "Unable to Init GEconStoreCategoryManager \n" ); +#endif + return false; + } + + // Initialize map of currency price points. + if ( !BInitializeCurrencyPricePoints( m_mapCurrencyPricePoints, pKVRoot->FindKey( "inventoryvalvegcpricesheet" ) ) ) + { +#ifdef GC_DLL + EmitError( SPEW_GC, "Unable to Init CurrencyPricePoints \n" ); +#endif + return false; + } + + m_unFeaturedItemIndex = pKVRoot->GetInt( "featured_item_index", 0 ); + + // first time purchase gift + m_pStorePromotionFirstTimePurchaseItem = GetItemSchema()->GetItemDefinitionByName( pKVRoot->GetString( "promotion_first_time_purchase_gift" ) ); + m_pStorePromotionFirstTimeWebPurchaseItem = GetItemSchema()->GetItemDefinitionByName( pKVRoot->GetString( "promotion_first_time_web_purchase_gift" ) ); + + EUniverse eUniverse = GetUniverse(); + + m_unPreviewPeriod = pKVRoot->GetInt( eUniverse == k_EUniversePublic ? "preview_period" : "preview_period_nonpublic" ); + m_unBonusDiscountPeriod = pKVRoot->GetInt( eUniverse == k_EUniversePublic ? "bonus_discount_period" : "bonus_discount_period_nonpublic" ); + m_flPreviewPeriodDiscount = pKVRoot->GetFloat( "preview_period_discount" ); + +#ifdef ENABLE_STORE_RENTAL_BACKEND + KeyValues *pRentalPriceScalesKV = pKVRoot->FindKey( "rental_price_scale" ); + if ( pRentalPriceScalesKV ) + { + FOR_EACH_SUBKEY( pRentalPriceScalesKV, pCategoryKV ) + { + m_mapRentalPriceScales.InsertOrReplace( pCategoryKV->GetName(), pCategoryKV->GetFloat() ); + } + } +#endif + + memset( &m_StorePromotionSpendForFreeItem, 0, sizeof( m_StorePromotionSpendForFreeItem ) ); + KeyValues *pPromotionsKV = m_pKVRaw->FindKey( "promotion_spend_for_free_item" ); + if ( pPromotionsKV ) + { + item_price_t unFreeItemSpendAmountUSD = pPromotionsKV->GetInt( "price_threshold", 0 ); + FOR_EACH_CURRENCY( eCurrency ) + { + const price_point_map_key_t key = { unFreeItemSpendAmountUSD, eCurrency }; + const CurrencyPricePointMap_t::IndexType_t unIdx = m_mapCurrencyPricePoints.Find( key ); + if ( unIdx == m_mapCurrencyPricePoints.InvalidIndex() ) + { +#ifdef GC_DLL + EmitError( SPEW_GC, "Unable to Find Currency %s in Currency Map. Likely missing from inventoryvalvegcpricesheet.vdf \n", PchNameFromECurrency(eCurrency) ); +#endif + continue; + } + + m_StorePromotionSpendForFreeItem.m_rgusPriceThreshold[ eCurrency ] = m_mapCurrencyPricePoints[unIdx]; + } + +#if !defined(CLIENT_DLL) && !defined(GAME_DLL) + CEconItemSchema *pSchema = GEconManager()->GetItemSchema(); + m_StorePromotionSpendForFreeItem.m_pItemDef = pSchema->GetItemDefinitionByName( pPromotionsKV->GetString( "item_definition" ) ); + AssertMsg( m_StorePromotionSpendForFreeItem.m_pItemDef || !pPromotionsKV->GetString( "item_definition", NULL ), + "Could find not the specified item for \"promotion_spend_for_free_item\"" ); +#endif + } + + // Get the normal sections + KeyValues *pEntriesKV = m_pKVRaw->FindKey( "entries" ); + if ( pEntriesKV ) + { + FOR_EACH_TRUE_SUBKEY( pEntriesKV, pKVEntry ) + { + if ( !BInitEntryFromKV( pKVEntry ) ) + { +#ifdef GC_DLL + EmitError( SPEW_GC, "Unable to Find Entries in Currency KVP \n" ); +#endif + continue; + } + } + } + + // Get a list of Market entries to populate in to the 'store'. GC never cares or reads these items + // Only for Client +#ifdef CLIENT_DLL + KeyValues *pMarketEntriesKV = m_pKVRaw->FindKey( "market_entries" ); + if ( pMarketEntriesKV ) + { + FOR_EACH_TRUE_SUBKEY( pMarketEntriesKV, pKVEntry ) + { + if ( !BInitMarketEntryFromKV( pKVEntry ) ) + { + return false; + } + } + } + + KeyValues *pFeaturedItemsKV = m_pKVRaw->FindKey( "featured_items" ); + if ( pFeaturedItemsKV ) + { + FOR_EACH_SUBKEY( pFeaturedItemsKV, pKVEntry ) + { + const char *pszItemName = pKVEntry->GetName(); + const CEconItemDefinition *pDef = GetItemSchema()->GetItemDefinitionByName( pszItemName ); + if ( !pDef ) + { + AssertMsg1( 0, "Unable to find item: %s", pszItemName ); + continue; + } + + m_vecFeaturedItems.AddToTail( pDef->GetDefinitionIndex() ); + } + } +#endif + + // Generate a hash of all item def indices and cache it off + m_unHashForAllItems = CalculateHashFromItems(); + +#ifdef GC_DLL + // Parse the sales block on the GC. We'll use this to dynamically adjust prices. + KeyValues *pTimedSalesKV = m_pKVRaw->FindKey( "timed_sales" ); + if ( pTimedSalesKV ) + { + FOR_EACH_TRUE_SUBKEY( pTimedSalesKV, pKVSale ) + { + if ( !InitTimedSaleEntryFromKV( pKVSale ) ) + { + EmitError( SPEW_GC, "Unable to Init Timed Sale \n" ); + } + return false; + } + + // Verify that none of our timed sales have overlapping items. + if ( !VerifyTimedSaleEntries() ) + return false; + } +#endif // GC_DLL + + // Now that store entries are loaded, let the category manager do more stuff + if ( !GEconStoreCategoryManager()->BOnPriceSheetLoaded( this ) ) + return false; + + return true; +} + +uint32 CEconStorePriceSheet::CalculateHashFromItems() const +{ + CRC32_t unHash; + CRC32_Init( &unHash ); + + FOR_EACH_MAP_FAST( m_mapEntries, idx ) + { + const econ_store_entry_t &entry = m_mapEntries[idx]; + item_definition_index_t usDefIndexTmp = entry.GetItemDefinitionIndex(); + CRC32_ProcessBuffer( &unHash, &usDefIndexTmp, sizeof( usDefIndexTmp ) ); + } + + CRC32_Final( &unHash ); + + return (uint32)unHash; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool BInitializeStoreEntryPricePoints( econ_store_entry_t& out_entry, const CurrencyPricePointMap_t& mapCurrencyPricePoints, KeyValues *pKVPrice, int nSalePercent ) +{ + if ( !pKVPrice ) + return false; + + FOR_EACH_CURRENCY( eCurrency ) + { + const price_point_map_key_t key = { (item_price_t)pKVPrice->GetInt(), eCurrency }; + const CurrencyPricePointMap_t::IndexType_t unIdx = mapCurrencyPricePoints.Find( key ); + + // Looking for a price point that doesn't exist, or doesn't exist for this currency? + if ( unIdx == mapCurrencyPricePoints.InvalidIndex() ) + { +#ifdef GC_DLL + EmitError( SPEW_GC, "Unable to Find Currency %s in init price points. Currency is missing from inventoryvalvegcpricesheet.vdf \n", PchNameFromECurrency( eCurrency ) ); +#endif + continue; + } + + // Weird initialization pattern: we're making sure that the value we read from the KeyValues + // block is the value we're storing in memory. We do this to avoid integer conversion problems, + // especially overflow (!). + const item_price_t unPrice = mapCurrencyPricePoints[unIdx]; + out_entry.SetBasePrice( eCurrency, unPrice ); +#pragma warning(push) +#pragma warning(disable : 4389) + Assert( out_entry.GetBasePrice( eCurrency ) == unPrice ); // NOTE: DO NOT CAST unPrice - CHECKING FOR OVERFLOW +#pragma warning(pop) + + if ( ( nSalePercent > 0 ) && ( nSalePercent < 100 ) ) + { + const item_price_t unSalePrice = out_entry.CalculateSalePrice( &out_entry, eCurrency, (float)nSalePercent ); + out_entry.SetSalePrice( eCurrency, unSalePrice ); + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Parses the KV section piece and add a econ_store_entry_t +//----------------------------------------------------------------------------- +bool CEconStorePriceSheet::BInitEntryFromKV( KeyValues *pKVEntry ) +{ + const bool bIsPackItem = pKVEntry->GetBool( "is_pack_item", false ); + +#if defined( CLIENT_DLL ) + // Skip pack items on the client + if ( bIsPackItem ) + return true; +#endif + + econ_store_entry_t entry; + entry.InitCategoryTags( pKVEntry->GetString( "category_tags" ) ); + + const bool bIsNew = entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_New ); + entry.m_bLimited = entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_Limited ); + entry.m_bNew = bIsNew; + entry.m_bPreviewAllowed = !bIsNew && entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_Weapons ); + entry.m_bIsMarketItem = false; + const bool bIsHighlighted = entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_Highlighted ); + entry.m_bHighlighted = bIsHighlighted; + + if ( entry.GetCategoryTagCount() == 0 ) + { + AssertMsg1( 0, "Unable to get category_tags for entry: %s", pKVEntry->GetName() ); + return false; + } + + const CEconItemDefinition *pDef = GetItemSchema()->GetItemDefinitionByName( pKVEntry->GetString( "item_link" ) ); + if ( !pDef ) + { + AssertMsg1( 0, "Unable to find item: %s", pKVEntry->GetString( "item_link" ) ); + return false; + } + + entry.SetItemDefinitionIndex( pDef->GetDefinitionIndex() ); + + int iQuantity = pKVEntry->GetInt( "quantity", 1 ); + entry.SetQuantity( iQuantity ); + Assert( entry.GetQuantity() == iQuantity ); + + entry.SetSteamGiftPackageID( pKVEntry->GetUint64( "steam_gift_package_id", 0 ) ); + +#if defined( TF_CLIENT_DLL ) || defined( TF_GC_DLL ) + entry.SetDate( pDef->GetFirstSaleDate() ); +#else + entry.SetDate( pKVEntry->GetString( "date", "1/1/1900" ) ); +#endif + + // Is the item sold out of the store? + entry.m_bSoldOut = pKVEntry->GetBool( "sold_out", false ); + + // Is the item a pack item? + entry.m_bIsPackItem = bIsPackItem; + + int nSalePercent = pKVEntry->GetInt( "sale_percentage", 0 ); + if ( ( nSalePercent < 0 ) || ( nSalePercent >= 100 ) ) + { + AssertMsg( false, "Sale percentage for %s set to an invalid value: %d\n", pDef->GetDefinitionName(), nSalePercent ); + return false; + } + + if ( !BInitializeStoreEntryPricePoints( entry, m_mapCurrencyPricePoints, pKVEntry->FindKey( "base_price" ), nSalePercent ) ) + return false; + + m_mapEntries.InsertOrReplace( entry.GetItemDefinitionIndex(), entry ); + return true; +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: Parses the KV section piece and add a econ_store_entry_t +//----------------------------------------------------------------------------- +bool CEconStorePriceSheet::BInitMarketEntryFromKV( KeyValues *pKVEntry ) +{ + //"The Alien Cranium" + //{ + // "item_link" "The Alien Cranium" + // "category_tags" "Cosmetics+Halloween" + // "date" "11/13/2014" + // "base_price" "1299" + //} + + econ_store_entry_t entry; + entry.InitCategoryTags( pKVEntry->GetString( "category_tags" ) ); + + const bool bIsNew = entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_New ); + entry.m_bLimited = entry.IsListedInCategory( CEconStoreCategoryManager::k_CategoryID_Limited ); + entry.m_bNew = bIsNew; + entry.m_bPreviewAllowed = false; + entry.m_bIsMarketItem = true; + + if ( entry.GetCategoryTagCount() == 0 ) + { + AssertMsg1( 0, "Unable to get category_tags for entry: %s", pKVEntry->GetName() ); + return false; + } + + const CEconItemDefinition *pDef = GetItemSchema()->GetItemDefinitionByName( pKVEntry->GetString( "item_link" ) ); + if ( !pDef ) + { + AssertMsg1( 0, "Unable to find item: %s", pKVEntry->GetString( "item_link" ) ); + return false; + } + + entry.SetItemDefinitionIndex( pDef->GetDefinitionIndex() ); + entry.SetQuantity( 1 ); + entry.SetDate( pDef->GetFirstSaleDate() ); // FIXME? + + entry.m_bSoldOut = false; + entry.m_bIsPackItem = false; + + // Entry should never already exist in mapEntries + if ( m_mapEntries.Find( entry.GetItemDefinitionIndex() ) != m_mapEntries.InvalidIndex() ) + { + AssertMsg1( 0, "Market Entry already eixsts : %s", pKVEntry->GetString( "item_link" ) ); + return false; + } + + m_mapEntries.InsertOrReplace( entry.GetItemDefinitionIndex(), entry ); + return true; +} +#endif // CLIENT_DLL + +#ifdef GC_DLL +//----------------------------------------------------------------------------- +// Purpose: Parses the KV section piece and add a econ_store_entry_t +//----------------------------------------------------------------------------- +static RTime32 ConvertKVDateToRTime32( KeyValues *pKV, const char *pszKey ) +{ + Assert( pKV ); + Assert( pszKey ); + + const char *pszTime = pKV->GetString( pszKey, NULL ); + if ( !pszTime || !pszTime[0] ) + return 0; + + RTime32 unConvertedTime = CRTime::RTime32FromString( pszTime ); + if ( unConvertedTime == (RTime32)-1 ) + return 0; + + return unConvertedTime; +} + +bool CEconStorePriceSheet::InitTimedSaleEntryFromKV( KeyValues *pKVTimedSaleEntry ) +{ + Assert( pKVTimedSaleEntry ); + + econ_store_timed_sale_t TimedSale; + + TimedSale.m_bSaleCurrentlyActive = false; + TimedSale.m_sIdentifier = pKVTimedSaleEntry->GetName(); + + TimedSale.m_SaleStartTime = ConvertKVDateToRTime32( pKVTimedSaleEntry, "sale_start_date" ); + TimedSale.m_SaleEndTime = ConvertKVDateToRTime32( pKVTimedSaleEntry, "sale_end_date" ); + + // Sanity check -- make sure this sale lasts a greater-than-zero amount of time. This will also + // catch any cases where the end time was invalid and so returned 0. + if ( TimedSale.m_SaleStartTime >= TimedSale.m_SaleEndTime ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has invalid duration\n", pKVTimedSaleEntry->GetName() ); + return false; + } + + // Make sure the end time is also greater than 0. + if ( TimedSale.m_SaleStartTime <= 0 ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has invalid start time\n", pKVTimedSaleEntry->GetName() ); + return false; + } + + // What items does this sale apply to? + KeyValues *pKVSaleItems = pKVTimedSaleEntry->FindKey( "sale_items" ); + if ( !pKVSaleItems ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' missing \"sale_items\" section\n", pKVTimedSaleEntry->GetName() ); + return false; + } + + FOR_EACH_TRUE_SUBKEY( pKVSaleItems, pKVSaleItem ) + { + const char *pszName = pKVSaleItem->GetString( "name", NULL ); + if ( !pszName ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has invalid/missing item name for entry '%s'\n", pKVTimedSaleEntry->GetName(), pKVSaleItem->GetName() ); + return false; + } + + const CEconItemDefinition *pItemDef = GetItemSchema()->GetItemDefinitionByName( pszName ); + if ( !pItemDef ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has missing item named '%s' for entry '%s'\n", pKVTimedSaleEntry->GetName(), pszName, pKVSaleItem->GetName() ); + return false; + } + + const float fSalePercentageOff = pKVSaleItem->GetFloat( "sale_percentage_off", -1.0f ); + if ( fSalePercentageOff <= 0.0f || fSalePercentageOff > 100.0f ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has invalid sale percentage for item named '%s' for entry '%s'\n", pKVTimedSaleEntry->GetName(), pszName, pKVSaleItem->GetName() ); + return false; + } + + const econ_store_entry_t *pStoreEntry = GetEntry( pItemDef->GetDefinitionIndex() ); + if ( !pStoreEntry ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has item named '%s' for entry '%s' that is not in the store\n", pKVTimedSaleEntry->GetName(), pszName, pKVSaleItem->GetName() ); + return false; + } + + // Verify that no item in a timed sale is already in a hard-coded sale as well. This would break + // our fragile pricing math assumptions. + FOR_EACH_CURRENCY( eCurrency ) + { + if ( pStoreEntry->IsOnSale( eCurrency ) ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has item named '%s' for entry '%s' that hard-coded to be on sale (currency: %i)\n", pKVTimedSaleEntry->GetName(), pszName, pKVSaleItem->GetName(), eCurrency ); + return false; + } + } + + econ_store_timed_sale_item_t SaleItem; + SaleItem.m_unItemDef = pItemDef->GetDefinitionIndex(); + SaleItem.m_fPricePercentage = fSalePercentageOff; + + TimedSale.m_vecSaleItems.AddToTail( SaleItem ); + } + + + if ( TimedSale.m_vecSaleItems.Count() <= 0 ) + { + EG_ERROR( GCSDK::SPEW_GC, "Timed sale '%s' has no valid items to put on sale\n", pKVTimedSaleEntry->GetName() ); + return false; + } + + // We made it this far with no errors, so add this as a timed sale block if it actually affects + // any items. + m_vecTimedSales.AddToTail( TimedSale ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CEconStorePriceSheet::VerifyTimedSaleEntries() +{ + // We could write an interval tree and be smart about this, but Fletcher made + // faces at me when I suggested it so we just brute force it -- we find each + // item in each sale sequentially, find each sale that overlaps with the current + // sale, and make sure that it doesn't have the same item. + for ( int i = 0; i < m_vecTimedSales.Count(); i++ ) + { + const econ_store_timed_sale_t& BaseSale = m_vecTimedSales[i]; + Assert( BaseSale.m_vecSaleItems.Count() > 0 ); + + for ( int j = 0; j < BaseSale.m_vecSaleItems.Count(); j++ ) + { + const item_definition_index_t unSearchDefIndex = BaseSale.m_vecSaleItems[j].m_unItemDef; + + for ( int k = i; k < m_vecTimedSales.Count(); k++ ) + { + // Does this sale overlap with our current sale? + const econ_store_timed_sale_t& OtherSale = m_vecTimedSales[k]; + + if ( k == i || MAX( BaseSale.m_SaleStartTime, OtherSale.m_SaleStartTime ) <= MIN( BaseSale.m_SaleEndTime, OtherSale.m_SaleEndTime ) ) + { + // We overlap, so make sure this item doesn't show up in both lists. Start the search in our + // current sale to make sure the same entry doesn't show up twice and then look at the full + // list of items in other overlapping sales. + for ( int l = (k == i ? j + 1 : 0); l < OtherSale.m_vecSaleItems.Count(); l++ ) + { + const item_definition_index_t unMaybeMatchDefIndex = OtherSale.m_vecSaleItems[l].m_unItemDef; + if ( unSearchDefIndex == unMaybeMatchDefIndex ) + { + EG_ERROR( GCSDK::SPEW_GC, "Conflict detected for item index '%i' betweens timed sales '%s' / '%s'\n", + unSearchDefIndex, + BaseSale.m_sIdentifier.Get(), + OtherSale.m_sIdentifier.Get() ); + return false; + } + } + } + } + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEconStorePriceSheet::UpdatePricesForTimedSales( const RTime32 curTime ) +{ + FOR_EACH_VEC( m_vecTimedSales, i ) + { + econ_store_timed_sale_t& TimedSale = m_vecTimedSales[i]; + Assert( TimedSale.m_vecSaleItems.Count() > 0 ); + + // Is this sale active in our current time? + const bool bSaleShouldBeActive = (curTime >= TimedSale.m_SaleStartTime) + && (curTime <= TimedSale.m_SaleEndTime); + + // Last time we processed it, was this sale active? + const bool bSaleIsActive = TimedSale.m_bSaleCurrentlyActive; + + // State transition? + if ( bSaleShouldBeActive != bSaleIsActive ) + { + FOR_EACH_VEC( TimedSale.m_vecSaleItems, i ) + { + econ_store_entry_t *unSaleStoreEntry = GetEntryWriteable( TimedSale.m_vecSaleItems[i].m_unItemDef ); + Assert( unSaleStoreEntry ); + + // We don't support items being on sale in multiple ways at the same time (ie., a + // sale specified in the base prices in store.txt and also a timed sale) so we just stomp + // whatever the sale price is with the price we think we should be on sale for. + FOR_EACH_CURRENCY( eCurrency ) + { + unSaleStoreEntry->SetSalePrice( eCurrency, + bSaleIsActive ? unSaleStoreEntry->GetBasePrice( eCurrency ) * TimedSale.m_vecSaleItems[i].m_fPricePercentage : 0 ); + } + } + + // Update our last-updated timestamp if any of these sales changed -- use the most recent + // "start of sale" date. This will allow people using the store prices WebAPI to know whether + // prices have changed. + + // If items just went on sale, their price changed at the start of this sale. + if ( bSaleIsActive ) + { + m_RTimeVersionStamp = MAX( m_RTimeVersionStamp, TimedSale.m_SaleStartTime ); + } + + // If items just got taken off sale, their price changed at the end of this sale. + else // if ( !bSaleIsActive ) + { + m_RTimeVersionStamp = MAX( m_RTimeVersionStamp, TimedSale.m_SaleEndTime ); + } + + // Update our cached state. + TimedSale.m_bSaleCurrentlyActive = bSaleShouldBeActive; + + // Debug output. + EmitInfo( GCSDK::SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "Timed sale '%s' has been %s.\n", TimedSale.m_sIdentifier.Get(), bSaleShouldBeActive ? "enabled" : "disabled" ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEconStorePriceSheet::DumpTimeSaleState( const RTime32 curTime ) const +{ + char curTimeBuf[k_RTimeRenderBufferSize]; + EmitInfo( GCSDK::SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "Current sale calculation time: %s\n", CRTime::RTime32ToString( curTime, curTimeBuf ) ); + + FOR_EACH_VEC( m_vecTimedSales, i ) + { + const econ_store_timed_sale_t& TimedSale = m_vecTimedSales[i]; + Assert( TimedSale.m_vecSaleItems.Count() > 0 ); + + if ( !TimedSale.m_bSaleCurrentlyActive ) + { + EmitInfo( GCSDK::SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "\tSale '%s' (not active)\n", TimedSale.m_sIdentifier.Get() ); + } + else + { + EmitInfo( GCSDK::SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "\tSale '%s' (active):\n", TimedSale.m_sIdentifier.Get() ); + + FOR_EACH_VEC( TimedSale.m_vecSaleItems, i ) + { + EmitInfo( GCSDK::SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "\t\t%s is %.0f%% off\n", + GetItemSchema()->GetItemDefinition( TimedSale.m_vecSaleItems[i].m_unItemDef )->GetDefinitionName(), + TimedSale.m_vecSaleItems[i].m_fPricePercentage ); + } + } + } +} +#endif // GC_DLL + +//----------------------------------------------------------------------------- +// Performs calculation of a discounted price given a base price, and will then handle ensuring the appropriate number of zeros at the end of the price via rounding +//----------------------------------------------------------------------------- +/*static*/ item_price_t econ_store_entry_t::GetDiscountedPrice( ECurrency eCurrency, item_price_t unBasePrice, float fDiscountPercentage ) +{ + Assert( fDiscountPercentage > 0.0f ); + Assert( fDiscountPercentage < 100.0f ); + + //if the base price is zero, it should never be anything but zero + if( unBasePrice == 0 ) + return unBasePrice; + + item_price_t unNewPrice = ( item_price_t )( ( double )unBasePrice * ( clamp( 100.0 - fDiscountPercentage, 0.0, 100.0 ) / 100.0 ) ); + + //determine what the unit of granularity we want to use for pricing. For example if you use 1, we keep 1/100th level precision, if you use 100 we'll round it to the nearest 100th (in USD this would be round to + //the nearest dollar, etc) + item_price_t unPriceGranularity = 1; + switch ( eCurrency ) + { + case k_ECurrencyRUB: unPriceGranularity = 100; break; + case k_ECurrencyJPY: unPriceGranularity = 100; break; + case k_ECurrencyNOK: unPriceGranularity = 100; break; + case k_ECurrencyPHP: unPriceGranularity = 100; break; + case k_ECurrencyTHB: unPriceGranularity = 100; break; + case k_ECurrencyKRW: unPriceGranularity = 1000; break; + case k_ECurrencyUAH: unPriceGranularity = 10; break; + case k_ECurrencyIDR: unPriceGranularity = 100; break; + case k_ECurrencyVND: unPriceGranularity = 1000; break; + case k_ECurrencyCNY: unPriceGranularity = 100; break; + case k_ECurrencyTWD: unPriceGranularity = 100; break; + case k_ECurrencyINR: unPriceGranularity = 100; break; + case k_ECurrencyCOP: unPriceGranularity = 100; break; + case k_ECurrencyCLP: unPriceGranularity = 100; break; + } + + + //now handle the rounding to the specified price granularity + if( unPriceGranularity > 1 ) + { + const item_price_t unRemainder = unNewPrice % unPriceGranularity; + //make sure to never let the price go to zero though + if( ( unRemainder >= unPriceGranularity / 2 ) || ( unNewPrice < unPriceGranularity ) ) + { + //round up + unNewPrice += unPriceGranularity - unRemainder; + } + else + { + //round down + unNewPrice -= unRemainder; + } + + //sanity check + Assert( ( unNewPrice % unPriceGranularity == 0 ) && ( unNewPrice > 0 ) ); + } + + return unNewPrice; +} + +//----------------------------------------------------------------------------- +// Purpose: This will calculate what the sale price will be for a particular +// item -- the result should be non-zero, but this does not mean the item is +// currently on sale. You can optionally pass in a pointer to a uint32 which +// will get you the actual discount percentage, which can be different for +// NXP, which has its own nonlinear pricing structure. +//----------------------------------------------------------------------------- +/*static*/ item_price_t econ_store_entry_t::CalculateSalePrice( const econ_store_entry_t* pSaleStoreEntry, ECurrency eCurrency, float fDiscountPercentage, int32 *out_pAdjustedDiscountPercentage/*=NULL*/ ) +{ + item_price_t unSalePrice = 0; +/* + // TF2 doesn't support NXP or RMB yet + if ( eCurrency == k_ECurrencyNXP || eCurrency == k_ECurrencyRMB ) + { + // For these currencies, we calculate the sale price based on the discount percentage times the *USD* base price -- rather than the discount percentage + // times the base price for the given currency. + const item_price_t unSalePrice_USD = econ_store_entry_t::GetDiscountedPrice( k_ECurrencyUSD, pSaleStoreEntry->GetBasePrice( k_ECurrencyUSD ), fDiscountPercentage ); + unSalePrice = ( eCurrency == k_ECurrencyNXP ) ? ConvertUSDToNXP( unSalePrice_USD ) : ConvertUSDToRMB( unSalePrice_USD ); + + // Ensure that the sale price is strictly less + Assert( unSalePrice < pSaleStoreEntry->GetBasePrice( eCurrency ) ); + if ( unSalePrice >= pSaleStoreEntry->GetBasePrice( eCurrency ) ) + { + unSalePrice = pSaleStoreEntry->GetBasePrice( eCurrency ); + } + } + else +*/ + { + unSalePrice = econ_store_entry_t::GetDiscountedPrice( eCurrency, pSaleStoreEntry->GetBasePrice( eCurrency ), fDiscountPercentage ); + } + + Assert( unSalePrice > 0 ); + + // Also set a percentage per currency, since they can be different. RMB and NXP, for example, + // calculate a sale price based on the USD sale price. + const bool bUseDefaultDiscountPercentage = ( eCurrency == k_ECurrencyUSD ); + const double fActualPercent = 100.0 * ( 1.0 - ( double )unSalePrice / ( double )pSaleStoreEntry->GetBasePrice( eCurrency ) ); + + const int32 nDiscountPercentageForCurrency = bUseDefaultDiscountPercentage ? + RoundFloatToInt( fDiscountPercentage ) : + RoundFloatToInt( fActualPercent ); + AssertMsg( nDiscountPercentageForCurrency >= 0 && nDiscountPercentageForCurrency < 100, "Invalid discount percentage of %u specified for item %u currency %u", nDiscountPercentageForCurrency, pSaleStoreEntry->GetItemDefinitionIndex(), eCurrency ); + + if ( out_pAdjustedDiscountPercentage ) + { + *out_pAdjustedDiscountPercentage = nDiscountPercentageForCurrency; + } + + return unSalePrice; +} + +//---------------------------------------------------------------------------- +// Purpose: Sort Entries by Name +//---------------------------------------------------------------------------- +int ItemNameSortComparator( const econ_store_entry_t *const *ppEntryA, const econ_store_entry_t *const *ppEntryB ) +{ + Assert( ppEntryA ); + Assert( ppEntryB ); + Assert( *ppEntryA ); + Assert( *ppEntryB ); + + const CEconItemDefinition *pItemDefA = GetItemSchema()->GetItemDefinition( (*ppEntryA)->GetItemDefinitionIndex() ); + const CEconItemDefinition *pItemDefB = GetItemSchema()->GetItemDefinition( (*ppEntryB)->GetItemDefinitionIndex() ); + + int nComp = Q_stricmp( pItemDefA->GetItemTypeName(), pItemDefB->GetItemTypeName() ); + if ( nComp != 0 ) + return nComp; + + // If the type matches, then sort by the item name + return Q_stricmp( pItemDefA->GetItemBaseName(), pItemDefB->GetItemBaseName() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sort by whatever sort type our price sheet is requesting +//----------------------------------------------------------------------------- +int FirstSaleDateSortComparator( const econ_store_entry_t *const *ppItemA, const econ_store_entry_t *const *ppItemB ) +{ + Assert( ppItemA ); + Assert( ppItemB ); + Assert( *ppItemA ); + Assert( *ppItemB ); + + const CEconItemDefinition *pItemDefA = GetItemSchema()->GetItemDefinition( (*ppItemA)->GetItemDefinitionIndex() ); + const CEconItemDefinition *pItemDefB = GetItemSchema()->GetItemDefinition( (*ppItemB)->GetItemDefinitionIndex() ); + Assert( pItemDefA ); + Assert( pItemDefB ); + + const char *pDateAddedA = pItemDefA->GetFirstSaleDate(); + const char *pDateAddedB = pItemDefB->GetFirstSaleDate(); + + return -strcmp( pDateAddedA, pDateAddedB ); +} + +bool CEconStoreEntryLess::Less( const uint16& lhs, const uint16& rhs, void *pContext ) +{ + CEconItemSchema *pSchema = GetItemSchema(); + + CEconStorePriceSheet* pPriceSheet = (CEconStorePriceSheet*)pContext; + eEconStoreSortType sortType = pPriceSheet->GetEconStoreSortType(); + const econ_store_entry_t *pItemA = pPriceSheet->GetEntry( lhs ); + const econ_store_entry_t *pItemB = pPriceSheet->GetEntry( rhs ); + Assert( pItemA ); + Assert( pItemB ); + + CEconItemDefinition *pItemDefA = pSchema->GetItemDefinition( pItemA->GetItemDefinitionIndex() ); + CEconItemDefinition *pItemDefB = pSchema->GetItemDefinition( pItemB->GetItemDefinitionIndex() ); + +#ifdef CLIENT_DLL + ECurrency eCurrency = EconUI()->GetStorePanel()->GetCurrency(); +#else + ECurrency eCurrency = k_ECurrencyUSD; +#endif + + if ( pItemDefA && pItemDefB ) + { + switch ( sortType ) + { + case kEconStoreSortType_Price_HighestToLowest: + if ( pItemA->GetCurrentPrice( eCurrency ) == pItemB->GetCurrentPrice( eCurrency ) ) + { + return Q_strcmp( pItemDefA->GetItemBaseName(), pItemDefB->GetItemBaseName() ) < 0; + } + return pItemA->GetCurrentPrice( eCurrency ) > pItemB->GetCurrentPrice( eCurrency ); + + case kEconStoreSortType_Price_LowestToHighest: + if ( pItemA->GetCurrentPrice( eCurrency ) == pItemB->GetCurrentPrice( eCurrency ) ) + { + return Q_strcmp( pItemDefA->GetItemBaseName(), pItemDefB->GetItemBaseName() ) < 0; + } + return pItemA->GetCurrentPrice( eCurrency ) < pItemB->GetCurrentPrice( eCurrency ); + + case kEconStoreSortType_DevName_AToZ: + return Q_strcmp( pItemDefA->GetItemBaseName(), pItemDefB->GetItemBaseName() ) < 0; + + case kEconStoreSortType_DevName_ZToA: + return Q_strcmp( pItemDefA->GetItemBaseName(), pItemDefB->GetItemBaseName() ) > 0; + + case kEconStoreSortType_DateNewest: + case kEconStoreSortType_DateOldest: + { + int iSortResult = FirstSaleDateSortComparator( &pItemA, &pItemB ); + + if ( iSortResult < 0 ) + return sortType == kEconStoreSortType_DateNewest; + + if ( iSortResult > 0 ) + return sortType == kEconStoreSortType_DateOldest; + + // Intentionally fall through to sorting by localized name if our dates match. + } + + case kEconStoreSortType_Name_AToZ: + case kEconStoreSortType_Name_ZToA: + if ( g_pVGuiLocalize ) + { + wchar_t *wszItemNameA = g_pVGuiLocalize->Find( pItemDefA->GetItemBaseName() ); + wchar_t *wszItemNameB = g_pVGuiLocalize->Find( pItemDefB->GetItemBaseName() ); + if ( wszItemNameA && wszItemNameB ) + { + // Note: locale-savvy string sorting uses wcscoll, not wcscmp + return sortType == kEconStoreSortType_Name_ZToA + ? wcscoll( wszItemNameA, wszItemNameB ) > 0 // only sort in reverse alphabetical order if asked to -- fall-through cases uses forward order + : wcscoll( wszItemNameA, wszItemNameB ) < 0; + } + } + break; + + case kEconStoreSortType_ItemDefIndex: + return pItemDefA->GetDefinitionIndex() < pItemDefB->GetDefinitionIndex(); + + case kEconStoreSortType_ReverseItemDefIndex: + return pItemDefB->GetDefinitionIndex() < pItemDefA->GetDefinitionIndex(); + } + } + + // default, highest to lowest price + return pItemA->GetCurrentPrice( eCurrency ) > pItemB->GetCurrentPrice( eCurrency ); +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: return the CLocale name that works with setlocale() +//----------------------------------------------------------------------------- +const char *GetLanguageCLocaleName( ELanguage eLang ) +{ + if ( eLang == k_Lang_None ) + return ""; + +#ifdef _WIN32 + // table for Win32 is here: http://msdn.microsoft.com/en-us/library/hzz3tw78(v=VS.80).aspx + // shortname works except for chinese + + switch ( eLang ) + { + case k_Lang_Simplified_Chinese: + return "chs"; // or "chinese-simplified" + case k_Lang_Traditional_Chinese: + return "cht"; // or "chinese-traditional" + case k_Lang_Korean: + return "korean"; // steam likes "koreana" for the name for some reason. + case k_Lang_Brazilian: + return "ptb"; // "portuguese-brazil" - that string fails even though it's in the MS lang table; ptb does work. + default: + return GetLanguageShortName( eLang ); + } + +#else + switch ( eLang ) + { + case k_Lang_Simplified_Chinese: + case k_Lang_Traditional_Chinese: + return "zh_CN"; + default: + ; + } + + // ICU codes work on linux/osx + return GetLanguageICUName( eLang ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Get an I/O stream into the right local/settings for printing money - so to speak +//----------------------------------------------------------------------------- +static void InitStreamLocale( std::wostringstream &stream, ELanguage eLang, uint32 nExpectedAmount, ECurrency eCurrencyCode ) +{ + const char *pszLocale = GetLanguageCLocaleName( eLang ); + +#ifdef _PS3 + stream.imbue(std::locale(pszLocale)); // no exception for PS3 +#else + try + { + stream.imbue(std::locale(pszLocale)); + } + catch (const std::exception &e) + { + Log( "stream::imbue() failed with locale: '%s', exception: %s\n", pszLocale, e.what() ); + stream.imbue( std::locale("C") ); + } +#endif + + // Don't display fractional rubles + // But if our amount is fractional, we should show it regardless + // hack hack - certain currencies should not show fractional symbol - see if we can wire this through from config at some point + if ( ( eCurrencyCode == k_ECurrencyRUB || eCurrencyCode == k_ECurrencyJPY || eCurrencyCode == k_ECurrencyIDR || eCurrencyCode == k_ECurrencyKRW ) + && nExpectedAmount % 100 == 0 ) + { + stream.precision( 0 ); + stream.setf( std::ios_base::fixed ); + } + else + { + stream.precision( 2 ); + stream.setf( std::ios_base::fixed, std::ios_base::floatfield ); + } +} + +// +// Partial Integration from \steam\main\src\common\amount.cpp +// +int MakeMoneyStringInternal( wchar_t *pchDest, uint32 nDest, item_price_t unPrice, ECurrency eCurrencyCode, ELanguage eLanguage ) +{ + // Use the actual currency symbol with the local number formatting. + // assume local locale - should not be used from server to send to client + // without passing in a valid pszCLocale parameter. + std::wostringstream stream; + InitStreamLocale( stream, eLanguage, unPrice, eCurrencyCode ); + + stream << (unPrice/100.0); + + const auto sAmount = stream.str(); + const wchar_t *wszAmount = sAmount.c_str(); + + // this code will be used by the client. An old client might encounter a currency code it doesn't know about. Handle that. + if ( eCurrencyCode == k_ECurrencyInvalid ) + { + // just return the value + return V_snwprintf( pchDest, nDest, L"%ls", wszAmount ); + } + + // select symbol + const char *pchSymbol = ""; + switch( eCurrencyCode ) + { + case k_ECurrencyUSD: + pchSymbol = "$"; + break; + + case k_ECurrencyGBP: + pchSymbol = "\xC2\xA3"; + break; + + case k_ECurrencyEUR: + pchSymbol = "\xE2\x82\xAC"; + break; + + case k_ECurrencyCHF: + pchSymbol = "CHF"; + break; + + case k_ECurrencyRUB: + pchSymbol = "\xD1\x80\xD1\x83\xD0\xB1"; // localized py6 + break; + + case k_ECurrencyBRL: + pchSymbol = "R$"; + break; + + case k_ECurrencyJPY: + pchSymbol = "\xC2\xA5"; + break; + + case k_ECurrencyIDR: + pchSymbol = "Rp"; + break; + + case k_ECurrencyMYR: + pchSymbol = "RM"; + break; + + case k_ECurrencyPHP: + pchSymbol = "\xE2\x82\xB1"; + break; + + case k_ECurrencySGD: + pchSymbol = "S$"; + break; + + case k_ECurrencyTHB: + pchSymbol = "\xE0\xB8\xBF"; + break; + + case k_ECurrencyVND: + pchSymbol = "\xE2\x82\xAB"; + break; + + case k_ECurrencyKRW: + pchSymbol = "\xe2\x82\xa9"; + break; + + case k_ECurrencyTRY: + pchSymbol = "TL"; + break; + + case k_ECurrencyUAH: + pchSymbol = "\xe2\x82\xb4"; + break; + + case k_ECurrencyMXN: + pchSymbol = "Mex$"; + break; + + case k_ECurrencyCAD: + pchSymbol = "C$"; + break; + + case k_ECurrencyAUD: + pchSymbol = "A$"; + break; + + case k_ECurrencyNZD: + pchSymbol = "NZ$"; + break; + + case k_ECurrencyNOK: + pchSymbol = "kr"; + break; + + case k_ECurrencyPLN: + pchSymbol = "z\xc5\x82"; + break; + + case k_ECurrencyCNY: + pchSymbol = "\xc2\xa5"; + break; + + case k_ECurrencyINR: + pchSymbol = "\xe2\x82\xb9"; + break; + + case k_ECurrencyCLP: + pchSymbol = "$"; // bugbug - prefix it with CLP? + break; + + case k_ECurrencyPEN: + pchSymbol = "S/."; + break; + + case k_ECurrencyCOP: + pchSymbol = "COL$"; + break; + + case k_ECurrencyZAR: + pchSymbol = "R"; + break; + + case k_ECurrencyHKD: + pchSymbol = "HK$"; + break; + + case k_ECurrencyTWD: + pchSymbol = "NT$"; + break; + + case k_ECurrencySAR: + pchSymbol = "SR"; + break; + + case k_ECurrencyAED: + pchSymbol = "DH"; + break; + + default: + AssertMsg( false, "Unknown currency code" ); + pchSymbol = "$"; + break; + } + + // BEGIN HACK GAME CLIENT CHARACTER SET CONVERSION + wchar_t wsSymbol[ 16 ]; + V_UTF8ToUnicode( pchSymbol, wsSymbol, ARRAYSIZE( wsSymbol ) ); + // END HACK GAME CLIENT CHARACTER SET CONVERSION + + bool bFirstSymbolThenAmount = true; // Whether to show "$5" or "5E" + bool bSpaceBetweenTokens = false; // Whether to render a space between tokens like "$5" has no space, but "5 pyb" has a space + + switch ( eCurrencyCode ) + { + case k_ECurrencyEUR: + case k_ECurrencyUAH: + bFirstSymbolThenAmount = false; + bSpaceBetweenTokens = false; + break; + case k_ECurrencyRUB: + case k_ECurrencyVND: + case k_ECurrencyNOK: + case k_ECurrencyTRY: + case k_ECurrencyPLN: + case k_ECurrencySAR: + case k_ECurrencyAED: + bFirstSymbolThenAmount = false; + bSpaceBetweenTokens = true; + break; + case k_ECurrencyIDR: + case k_ECurrencyMXN: + case k_ECurrencyCAD: + case k_ECurrencyAUD: + case k_ECurrencyNZD: + case k_ECurrencyPEN: + case k_ECurrencyCOP: + case k_ECurrencyZAR: + case k_ECurrencyHKD: + case k_ECurrencyTWD: + case k_ECurrencyKRW: + case k_ECurrencyCHF: + bFirstSymbolThenAmount = true; + bSpaceBetweenTokens = true; + default: + bFirstSymbolThenAmount = true; + bSpaceBetweenTokens = false; + } + + return V_snwprintf( pchDest, nDest, L"%ls%ls%ls", + ( bFirstSymbolThenAmount ? wsSymbol : wszAmount ), + ( bSpaceBetweenTokens ? L" " : L"" ), + ( bFirstSymbolThenAmount ? wszAmount : wsSymbol ) ); +} + +static ELanguage GetStoreLanguage() +{ + if ( !engine ) + return k_Lang_English; + + char uilanguage[ 64 ]; + uilanguage[0] = 0; + engine->GetUILanguage( uilanguage, sizeof( uilanguage ) ); + + return PchLanguageToELanguage( uilanguage ); +} + +void MakeMoneyString( wchar_t *pchDest, uint32 nDest, item_price_t unPrice, ECurrency eCurrencyCode ) +{ + (void)MakeMoneyStringInternal( pchDest, nDest, unPrice, eCurrencyCode, GetStoreLanguage() ); +} +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CEconStorePriceSheet::BItemExistsInPriceSheet( item_definition_index_t unDefIndex ) const +{ + CEconStoreCategoryManager *pCategoryManager = GEconStoreCategoryManager(); + const int nCategoryCount = pCategoryManager->GetNumCategories(); + + for ( int i = 0; i < nCategoryCount; ++i ) + { + const CEconStoreCategoryManager::StoreCategory_t *pCat = pCategoryManager->GetCategoryFromIndex( i ); + + // Intentionally not using CUtlSortVector<>::Find(), since it calls Less(), which is slow as shit for m_vecEntries. + FOR_EACH_VEC( pCat->m_vecEntries, j ) + { + if ( pCat->m_vecEntries[j] == unDefIndex ) + return true; + } + } + + return false; +} + +#ifdef CLIENT_DLL +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool ShouldUseNewStore() +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int GetStoreVersion() +{ + return 2; +} +#endif // CLIENT_DLL + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const CEconStorePriceSheet *GetEconPriceSheet() +{ +#ifdef GC_DLL + return GEconManager()->GetPriceSheet(); +#else + return EconUI() && EconUI()->GetStorePanel() + ? EconUI()->GetStorePanel()->GetPriceSheet() + : NULL; +#endif +} + |