//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Common objects and utilities related to the in-game item store // //============================================================================= #ifndef ECON_STORE_H #define ECON_STORE_H #ifdef _WIN32 #pragma once #endif #include "UtlSortVector.h" #include "vstdlib/IKeyValuesSystem.h" #include "econ/econ_storecategory.h" #ifdef CLIENT_DLL #include "client_community_market.h" #endif // CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: Error code enum for purchase messages //----------------------------------------------------------------------------- enum EPurchaseResult { k_EPurchaseResultOK = 1, // Success k_EPurchaseResultFail = 2, // Generic error k_EPurchaseResultInvalidParam = 3, // Invalid parameter k_EPurchaseResultInternalError = 4, // Internal error k_EPurchaseResultNotApproved = 5, // Tried to finalize a transaction that has not yet been approved k_EPurchaseResultAlreadyCommitted = 6, // Tried to finalize a transaction that has already been committed k_EPurchaseResultUserNotLoggedIn = 7, // User is not logged into Steam k_EPurchaseResultWrongCurrency = 8, // Microtransaction's currency does not match user's wallet currency k_EPurchaseResultAccountError = 9, // User's account does not exist or is temporarily unavailable k_EPurchaseResultInvalidItem = 10, // User is trying to purchase an item that doesn't exist or is not for sale k_EPurchaseResultNotEnoughBackpackSpace = 11, // User did not have enough backpack space k_EPurchaseResultLimitedQuantityItemsUnavailable = 12, // User tried to purchase limited-quantity items but there weren't enough left in stock k_EPurchaseResultInsufficientFunds = 100, // User does not have wallet funds k_EPurchaseResultTimedOut = 101, // Time limit for finalization has been exceeded k_EPurchaseResultAcctDisabled = 102, // Steam account is disabled k_EPurchaseResultAcctCannotPurchase = 103, // Steam account is not allowed to make a purchase k_EMicroTxnResultFailedFraudChecks = 104, // Fraud checks inside of Steam failed k_EPurchaseResultOldPriceSheet = 150, // Information on the purchase didn't match the current price sheet k_EPurchaseResultTxnNotFound = 151 // Could not find the transaction specified }; const char *PchNameFromEPurchaseResult( EPurchaseResult ePurchaseState ); //----------------------------------------------------------------------------- // Purpose: State of a transaction // // WARNING: VALUES STORED IN DATABASE. DO NOT RENUMBER!!! //----------------------------------------------------------------------------- enum EPurchaseState { k_EPurchaseStateInvalid = 0, // Invalid k_EPurchaseStateInit = 1, // We have sent InitPurchase to Steam k_EPurchaseStateWaitingForAuthorization = 2, // We have gotten initial authorization from Steam. Waiting for user to authorize. k_EPurchaseStatePending = 3, // We are attempting to commit the transaction k_EPurchaseStateComplete = 4, // The transaction was successful k_EPurchaseStateFailed = 5, // The transaction failed k_EPurchaseStateCanceled = 6, // The transaction was canceled k_EPurchaseStateRefunded = 7, // The transaction was refunded k_EPurchaseStateChargeback = 8, // The transaction was charged back k_EPurchaseStateChargebackReversed = 9, // A chargeback has failed and we got the money k_EPurchaseStateLast = k_EPurchaseStateChargebackReversed, }; const char *PchNameFromEPurchaseState( EPurchaseState ePurchaseState ); // DO NOT RENUMBER! These values are stored in the audit log table. enum EGCTransactionAuditReason { k_EGCTransactionAudit_GCTransactionCompleted = 0, // The transaction completed successfully on the GC. k_EGCTransactionAudit_GCTransactionInit = 1, // The transaction was initialized. k_EGCTransactionAudit_GCTransactionPostInit = 2, // The result of attempting to initialize the transaction. This is where the SteamTxnID is set. k_EGCTransactionAudit_GCTransactionFinalize = 3, // We have started to finalize the transaction (so probably set it to pending). k_EGCTransactionAudit_GCTransactionFinalizeFailed = 4, // Our attempt to finalize the transaction failed for some reason (not due to a timeout). k_EGCTransactionAudit_GCTransactionCanceled = 5, // The client requested that we cancel the transaction. k_EGCTransactionAudit_SteamFailedMismatch = 6, // Steam failed the transaction but we did not. k_EGCTransactionAudit_GCRemovePurchasedItems = 7, // We are attempting to remove the purchased items from their backpack (due to rollback or failure). k_EGCTransactionAudit_GCTransactionInsert = 8, // We are inserting a transaction, usually one created by a web interface (i.e.: a cd-key operation) instead of a standard store interaction. k_EGCTransactionAudit_GCTransactionCompletedPostChargeback = 9, // We thought this transaction had been charged back but Steam came back later and told us it was successful after all. k_EGCTransactionAuditLast = k_EGCTransactionAudit_GCTransactionCompletedPostChargeback, }; const char *PchNameFromEGCTransactionAuditReason( EGCTransactionAuditReason eAuditReason ); enum EGCTransactionAuditInsertReason { k_EGCTransactionAuditInsert_Invalid = 0, // k_EGCTransactionAuditInsert_CDKey = 1, // A CD Key was used for this transaction. k_EGCTransactionAuditInsert_SettlementNoMatch_Failed = 2, // We inserted a failed record during settlement to unblock the process. k_EGCTransactionAuditInsert_SettlementNoMatch_Pending = 3, // We inserted a pending record during settlement to unblock the process. k_EGCTransactionAuditInsertLast = k_EGCTransactionAuditInsert_SettlementNoMatch_Pending, }; //----------------------------------------------------------------------------- // Purpose: Currencies we support // // WARNING: VALUES STORED IN DATABASE. DO NOT RENUMBER!!! // WARNING: THESE DON'T MATCH THE STEAM NUMERIC IDS!!! WE TALK USING CURRENCY // CODES LIKE "VND" AND IF YOU SAY "15" TERRIBLE THINGS WILL HAPPEN //----------------------------------------------------------------------------- enum ECurrency { k_ECurrencyFirst = 0, k_ECurrencyUSD = 0, k_ECurrencyGBP = 1, k_ECurrencyEUR = 2, k_ECurrencyRUB = 3, k_ECurrencyBRL = 4, // space for Dota currencies k_ECurrencyJPY = 8, k_ECurrencyNOK = 9, k_ECurrencyIDR = 10, k_ECurrencyMYR = 11, k_ECurrencyPHP = 12, k_ECurrencySGD = 13, k_ECurrencyTHB = 14, k_ECurrencyVND = 15, k_ECurrencyKRW = 16, k_ECurrencyTRY = 17, k_ECurrencyUAH = 18, k_ECurrencyMXN = 19, k_ECurrencyCAD = 20, k_ECurrencyAUD = 21, k_ECurrencyNZD = 22, k_ECurrencyPLN = 23, k_ECurrencyCHF = 24, k_ECurrencyCNY = 25, k_ECurrencyTWD = 26, k_ECurrencyHKD = 27, k_ECurrencyINR = 28, k_ECurrencyAED = 29, k_ECurrencySAR = 30, k_ECurrencyZAR = 31, k_ECurrencyCOP = 32, k_ECurrencyPEN = 33, k_ECurrencyCLP = 34, // NOTE: Not actually the Maximum currency value, but the Terminator for the possible currency code range. k_ECurrencyMax = 35, // make this a big number so we can avoid having to move it when we add another currency type k_ECurrencyInvalid = 255, k_ECurrencyCDKeyTransaction = k_ECurrencyInvalid, }; // Macro for looping across all valid currencies #define FOR_EACH_CURRENCY( _i ) for ( ECurrency _i = GetFirstValidCurrency(); _i != k_ECurrencyInvalid; _i = GetNextValidCurrency( _i ) ) const char *PchNameFromECurrency( ECurrency eCurrency ); // NOTE: Defined with ENUMSTRINGS_START/ENUMSTRINGS_REVERSE macros ECurrency ECurrencyFromName( const char *pchName ); // inline bool BIsCurrencyValid( ECurrency eCurrency ) { switch ( eCurrency ) { case k_ECurrencyUSD: case k_ECurrencyGBP: case k_ECurrencyEUR: case k_ECurrencyRUB: case k_ECurrencyBRL: case k_ECurrencyJPY: case k_ECurrencyNOK: case k_ECurrencyIDR: case k_ECurrencyMYR: case k_ECurrencyPHP: case k_ECurrencySGD: case k_ECurrencyTHB: case k_ECurrencyVND: case k_ECurrencyKRW: case k_ECurrencyTRY: case k_ECurrencyUAH: case k_ECurrencyMXN: case k_ECurrencyCAD: case k_ECurrencyAUD: case k_ECurrencyNZD: //case k_ECurrencyPLN: case k_ECurrencyCHF: case k_ECurrencyCNY: case k_ECurrencyTWD: case k_ECurrencyHKD: case k_ECurrencyINR: case k_ECurrencyAED: case k_ECurrencySAR: case k_ECurrencyZAR: case k_ECurrencyCOP: case k_ECurrencyPEN: case k_ECurrencyCLP: return true; } return false; } inline ECurrency GetFirstValidCurrency() { for ( int i = k_ECurrencyFirst; i < k_ECurrencyMax; i++ ) { if ( BIsCurrencyValid( (ECurrency)i ) ) return (ECurrency)i; } return k_ECurrencyInvalid; } inline ECurrency GetNextValidCurrency( ECurrency ePrevious ) { for ( int i = ePrevious + 1; i < k_ECurrencyMax; i++ ) { if ( BIsCurrencyValid( (ECurrency)i ) ) return (ECurrency)i; } return k_ECurrencyInvalid; } //----------------------------------------------------------------------------- // Purpose: Simple struct for pairing a sort type with a localization string //----------------------------------------------------------------------------- struct ItemSortTypeData_t { const char *szSortDesc; // localization string uint32 iSortType; // maps to the GC-specific sort value }; // Description of a single item for sale struct econ_store_entry_t { // Constructor econ_store_entry_t() : m_pchCategoryTags( NULL ), m_unGiftSteamPackageID( 0 ), m_bHighlighted( false ) { V_memset( m_unBaseCosts, 0, sizeof(m_unBaseCosts) ); V_memset( m_unSaleCosts, 0, sizeof(m_unSaleCosts) ); } void SetItemDefinitionIndex( item_definition_index_t usDefIndex ); item_definition_index_t GetItemDefinitionIndex() const { return m_usDefIndex; } void InitCategoryTags( const char *pTags ); // Sets m_pchCategoryTags and initializes m_vecTagIds and m_fRentalPriceScale bool IsListedInCategory( StoreCategoryID_t unID ) const; // Is this item listed in the given category? bool IsListedInSubcategories( const CEconStoreCategoryManager::StoreCategory_t &Category ) const; // Is this item listed in one of Category's subcategories? bool IsListedInCategoryOrSubcategories( const CEconStoreCategoryManager::StoreCategory_t &Category ) const; // Is this item listed in Category or one of Category's subcategories? bool IsOnSale( ECurrency eCurrency ) const; bool IsRentable() const; #ifdef CLIENT_DLL bool HasDiscount( ECurrency eCurrency, item_price_t *out_punOptionalBasePrice ) const; // returns true if we're on sale or if we're a bundle with a discounted total price #endif // CLIENT_DLL item_price_t GetCurrentPrice( ECurrency eCurrency ) const; float GetRentalPriceScale() const; uint32 GetGiftSteamPackageID() const { return m_unGiftSteamPackageID; } // Helper function -- so we do this calculation in a single place. static item_price_t GetDiscountedPrice( ECurrency eCurrency, item_price_t unBasePrice, float fDiscountPercentage ); static item_price_t CalculateSalePrice( const econ_store_entry_t* pSaleStoreEntry, ECurrency eCurrency, float fDiscountPercentage, int32 *out_pAdjustedDiscountPercentage = NULL ); item_price_t GetBasePrice( ECurrency eCurrency ) const { Assert( eCurrency >= k_ECurrencyFirst ); Assert( eCurrency < k_ECurrencyMax ); if ( !( eCurrency >= 0 && eCurrency < k_ECurrencyMax ) ) return 0; #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 // Weird-looking pattern: we're making sure that the value we're about to return fits correctly // into the variable we're about to put it into. We do this to avoid integer conversion problems, // especially overflow (!) where someone changes one of the return type or the storage type but // not the other. Assert( (item_price_t)m_unBaseCosts[eCurrency] == m_unBaseCosts[eCurrency] ); return m_unBaseCosts[eCurrency]; } item_price_t GetSalePrice( ECurrency eCurrency ) const { Assert( eCurrency >= k_ECurrencyFirst ); Assert( eCurrency < k_ECurrencyMax ); if ( !( eCurrency >= 0 && eCurrency < k_ECurrencyMax ) ) return 0; #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 // Weird-looking pattern: we're making sure that the value we're about to return fits correctly // into the variable we're about to put it into. We do this to avoid integer conversion problems, // especially overflow (!) where someone changes one of the return type or the storage type but // not the other. Assert( (item_price_t)m_unSaleCosts[eCurrency] == m_unSaleCosts[eCurrency] ); return m_unSaleCosts[eCurrency]; } uint16 GetQuantity() const { return m_usQuantity; } const char* GetDate() const { return m_strDate.Get(); } bool CanPreview() const { // No previewing of new items or weapons. return m_bPreviewAllowed; } void SetQuantity( uint16 usQuantity ) { Assert( usQuantity > 0 ); m_usQuantity = usQuantity; } void ValidatePrice( ECurrency eCurrency, item_price_t unPrice ); void SetBasePrice( ECurrency eCurrency, item_price_t unPrice ) { Assert( eCurrency >= k_ECurrencyFirst ); Assert( eCurrency < k_ECurrencyMax ); if ( !( eCurrency >= 0 && eCurrency < k_ECurrencyMax ) ) return; ValidatePrice( eCurrency, unPrice ); m_unBaseCosts[eCurrency] = unPrice; } void SetSalePrice( ECurrency eCurrency, item_price_t unPrice ) { Assert( eCurrency >= k_ECurrencyFirst ); Assert( eCurrency < k_ECurrencyMax ); if ( !( eCurrency >= 0 && eCurrency < k_ECurrencyMax ) ) return; ValidatePrice( eCurrency, unPrice ); // It's legal to have a sale price of zero, meaninig "this item is not on sale" in this // currency. // Assert( unPrice > 0 ); m_unSaleCosts[eCurrency] = unPrice; } void SetSteamGiftPackageID( uint32 unGiftSteamPackageID ) { m_unGiftSteamPackageID = unGiftSteamPackageID; } void SetDate( const char* pszDate ) { m_strDate.Set( pszDate ); } bool IsValidCategoryTagIndex( uint32 iIndex ) const { AssertMsg( m_vecCategoryTags.IsValidIndex( iIndex ), "Category tag index out of range." ); return m_vecCategoryTags.IsValidIndex( iIndex ); } uint32 GetCategoryTagCount() const { return m_vecCategoryTags.Count(); } const char *GetCategoryTagNameFromIndex( uint32 iIndex ) const { if ( !IsValidCategoryTagIndex( iIndex ) ) return NULL; return m_vecCategoryTags[ iIndex ].m_strName; } StoreCategoryID_t GetCategoryTagIDFromIndex( uint32 iIndex ) const; const char *GetCategoryTagString() const { return m_pchCategoryTags; } bool m_bLimited; // Item is a limited sale bool m_bNew; // Item is new bool m_bHighlighted; // Item is highlighted CUtlString m_strDate; // Date Added bool m_bSoldOut; // True if the item is sold out from the store (for example if the item is a ticket or another physical item) bool m_bPreviewAllowed; // Is this item previewable? bool m_bIsPackItem; // Is this item a pack item? Pack items are items which are not individually for sale, but are sold via a bundle known as a "pack bundle" bool m_bIsMarketItem; // Is Market Item Link private: item_definition_index_t m_usDefIndex; // DefIndex of the item // Private data so that we can check in the accessor functions that the data fits before returning it. item_price_t m_unBaseCosts[k_ECurrencyMax]; // Costs of the items indexed by ECurrency -- if the items are on sale, this will be the current sale price item_price_t m_unSaleCosts[k_ECurrencyMax]; // Original costs of the items indexed by ECurrency -- if the items are on sale, this will be the pre-sale price uint16 m_usQuantity; // Quantity sold in a single purchase (ie., dueling pistols come in stacks of five) float m_fRentalPriceScale; // 100.0 or greater means "unavailable to rent" uint32 m_unGiftSteamPackageID; // if non-zero, when this item is purchased (including inside bundles, etc.), grant a gift copy of this Steam package struct CategoryTag_t { CUtlString m_strName; // Individual tag name, like "Weapons," "New," etc. StoreCategoryID_t m_unID; // The category ID }; CCopyableUtlVector< CategoryTag_t > m_vecCategoryTags; // Category tag data const char *m_pchCategoryTags; // All tags - this string will something like: "New" or "Weapons+New" etc. }; #ifdef GC_DLL struct econ_store_timed_sale_item_t { item_definition_index_t m_unItemDef; float m_fPricePercentage; // 100.0 = regular price; 50.0 = half price }; struct econ_store_timed_sale_t { bool m_bSaleCurrentlyActive; // set in ::UpdatePricesForTimedSales() CUtlConstString m_sIdentifier; // can't point to memory in the base KV because we toss it afterwards RTime32 m_SaleStartTime; RTime32 m_SaleEndTime; CUtlVector m_vecSaleItems; // Work around protected default vector constructor. econ_store_timed_sale_t() { } econ_store_timed_sale_t( const econ_store_timed_sale_t& other ) : m_bSaleCurrentlyActive( other.m_bSaleCurrentlyActive ) , m_sIdentifier( other.m_sIdentifier ) , m_SaleStartTime( other.m_SaleStartTime ) , m_SaleEndTime( other.m_SaleEndTime ) { m_vecSaleItems.CopyArray( other.m_vecSaleItems.Base(), other.m_vecSaleItems.Count() ); } }; #endif // GC_DLL // Spend xxx amount of money, get a free item from the loot list struct store_promotion_spend_for_free_item_t { const CEconItemDefinition *m_pItemDef; item_price_t m_rgusPriceThreshold[k_ECurrencyMax]; // Price threshold to get an item from the loot list indexed by ECurrency }; //----------------------------------------------------------------------------- // Purpose: Class that represents what's currently for sale in TF //----------------------------------------------------------------------------- typedef enum { kEconStoreSortType_Price_HighestToLowest = 0, kEconStoreSortType_Price_LowestToHighest = 1, kEconStoreSortType_DevName_AToZ = 2, kEconStoreSortType_DevName_ZToA = 3, kEconStoreSortType_Name_AToZ = 4, kEconStoreSortType_Name_ZToA = 5, kEconStoreSortType_ItemDefIndex = 6, kEconStoreSortType_ReverseItemDefIndex = 7, kEconStoreSortType_DateNewest = 8, kEconStoreSortType_DateOldest = 9, } eEconStoreSortType; struct price_point_map_key_t { item_price_t m_unPriceUSD; ECurrency m_eCurrency; static bool Less( const price_point_map_key_t& a, const price_point_map_key_t& b ) { if ( a.m_eCurrency == b.m_eCurrency ) return a.m_unPriceUSD < b.m_unPriceUSD; return a.m_eCurrency < b.m_eCurrency; } }; typedef CUtlMap CurrencyPricePointMap_t; class CEconStorePriceSheet { public: typedef CUtlMap StoreEntryMap_t; typedef CUtlMap RentalPriceScaleMap_t; typedef CUtlVector FeaturedItems_t; CEconStorePriceSheet(); ~CEconStorePriceSheet(); bool InitFromKV( KeyValues *pKVPrices ); // Gets or sets the version stamp. This is just a number the GC can use // to know if the client is in sync without sending it down on every // request. RTime32 GetVersionStamp( void ) const { return m_RTimeVersionStamp; } void SetVersionStamp( RTime32 stamp ) { m_RTimeVersionStamp = stamp; } uint32 GetHashForAllItems() const { return m_unHashForAllItems; } typedef CUtlMap EconStoreEntryMap_t; EconStoreEntryMap_t &GetEntries() { return m_mapEntries; } #ifdef GC_DLL econ_store_entry_t *GetEntryWriteable( item_definition_index_t unDefIndex ); #endif // GC_DLL const StoreEntryMap_t &GetEntries() const { return m_mapEntries; } const CEconStoreCategoryManager::StoreCategory_t *GetFeaturedItems( void ) { return &m_FeaturedItems; } const econ_store_entry_t *GetEntry( item_definition_index_t usDefIndex ) const; uint32 GetFeaturedItemIndex() const { return m_unFeaturedItemIndex; } void SetFeaturedItemIndex( uint32 unIdx ) { m_unFeaturedItemIndex = unIdx; } void SetEconStoreSortType( eEconStoreSortType eType ) { m_eEconStoreSortType = eType; } eEconStoreSortType GetEconStoreSortType() { return m_eEconStoreSortType; } const store_promotion_spend_for_free_item_t *GetStorePromotion_SpendForFreeItem() const { return &m_StorePromotionSpendForFreeItem; } const CEconItemDefinition * GetStorePromotion_FirstTimePurchaseItem() const { return m_pStorePromotionFirstTimePurchaseItem; } const CEconItemDefinition * GetStorePromotion_FirstTimeWebPurchaseItem() const { return m_pStorePromotionFirstTimeWebPurchaseItem; } uint32 GetPreviewPeriod() const { return m_unPreviewPeriod; } uint32 GetBonusDiscountPeriod() const { return m_unBonusDiscountPeriod; } float GetPreviewPeriodDiscount() const { return m_flPreviewPeriodDiscount; } bool BItemExistsInPriceSheet( item_definition_index_t unDefIndex ) const; float GetRentalPriceScale( const char *pszCategory ) const { RentalPriceScaleMap_t::IndexType_t i = m_mapRentalPriceScales.Find( pszCategory ); if ( i == RentalPriceScaleMap_t::InvalidIndex() ) return 1.0f; return m_mapRentalPriceScales[i]; } KeyValues *GetRawData() const { return m_pKVRaw; } #ifdef GC_DLL void UpdatePricesForTimedSales( const RTime32 curTime ); void DumpTimeSaleState( const RTime32 curTime ) const; #endif // GC_DLL #ifdef CLIENT_DLL const FeaturedItems_t& GetFeaturedItems() const { return m_vecFeaturedItems; } #endif // CLIENT_DLL private: bool BInitEntryFromKV( KeyValues *pKVEntry ); #ifdef CLIENT_DLL bool BInitMarketEntryFromKV( KeyValues *pKVEntry ); #endif // CLIENT_DLL #ifdef GC_DLL bool InitTimedSaleEntryFromKV( KeyValues *pKVTimedSaleEntry ); bool VerifyTimedSaleEntries(); #endif // GC_DLL private: void Clear(); uint32 CalculateHashFromItems() const; KeyValues *m_pKVRaw; RTime32 m_RTimeVersionStamp; CEconStoreCategoryManager::StoreCategory_t m_FeaturedItems; // Special section, not a tab, kept outside m_vecContents StoreEntryMap_t m_mapEntries; RentalPriceScaleMap_t m_mapRentalPriceScales; store_promotion_spend_for_free_item_t m_StorePromotionSpendForFreeItem; CEconItemDefinition* m_pStorePromotionFirstTimePurchaseItem; CEconItemDefinition* m_pStorePromotionFirstTimeWebPurchaseItem; #ifdef CLIENT_DLL FeaturedItems_t m_vecFeaturedItems; #endif // CLIENT_DLL #ifdef GC_DLL CUtlVector m_vecTimedSales; #endif // GC_DLL // changes based on experiments uint32 m_unFeaturedItemIndex; eEconStoreSortType m_eEconStoreSortType; uint32 m_unPreviewPeriod; uint32 m_unBonusDiscountPeriod; float m_flPreviewPeriodDiscount; uint32 m_unHashForAllItems; // price point lookup CurrencyPricePointMap_t m_mapCurrencyPricePoints; }; #ifdef CLIENT_DLL void MakeMoneyString( wchar_t *pchDest, uint32 nDest, item_price_t unPrice, ECurrency eCurrencyCode ); bool ShouldUseNewStore(); int GetStoreVersion(); #endif // CLIENT_DLL const CEconStorePriceSheet *GetEconPriceSheet(); #endif // ECON_STORE_H