function GetElemSNR( $Elem ) { var snr = $Elem.data( 'snr' ); if ( typeof snr != 'undefined' ) { return snr; } // look for links with snr parameter var links = $Elem.is( 'a' ) ? $Elem : $Elem.find( 'a' ); snr = null; for ( var i = 0; i < links.length; ++i ) { var link = links[i]; var navinfo = link.href.match( /[\?&]snr=([a-zA-Z0-9\-\_ ]+)/ ); if ( navinfo ) { snr = navinfo[1]; break; } } $Elem.data( 'snr', snr ); return snr; } // given an array of impressions as strings, this will handle joining them all together into a singular string, but enforcing that it doesn't // go above the cookie size limit which can otherwise cause users to become stuck since the page requests will start failing function JoinImpressionsUpToLimit( rgImpressions ) { //cookies generally can go up to 4k bytes, but we can have problems when we start getting that close, so cut it off earlier var nRemainingLen = 3200; var result = ''; for ( var i = 0; i < rgImpressions.length; i++ ) { var impression = String( rgImpressions[ i ] ); var nImpressionLen = encodeURIComponent( impression + '|' ).length; //did we run out of room in our list? if ( nRemainingLen < nImpressionLen ) break; //add the separator if not the first entry if ( result !== '' ) result += '|'; //add our impression and remove that space from what is available result += impression; nRemainingLen -= nImpressionLen; } return result; } GDynamicStore = { m_bLoadComplete: false, s_rgWishlist: {}, s_rgOwnedPackages: {}, s_rgOwnedApps: {}, s_rgMasterSubApps: {}, s_rgAutoGrantApps: {}, s_rgPackagesInCart: {}, s_rgAppsInCart: {}, s_rgRecommendedTags: [], s_rgIgnoredApps: {}, s_rgIgnoredPackages: {}, s_rgCurators: {}, s_rgCurations: {}, s_rgCreatorsFollowed: {}, s_rgCreatorsIgnored: {}, s_preferences: {}, s_rgExcludedTags: {}, s_rgExcludedDescIDs: {}, s_rgPersonalizedBundleData: {}, s_rgfnOnReadyCallbacks: [], s_rgDisplayedApps: [], s_rgDisplayedBundles: [], s_rgDisplayedPackages: [], s_bUserOnMacOS: false, s_bUserOnLinux: false, s_bUserOnWindows: false, s_rgRecommendedApps: [], s_ImpressionTracker: false, s_bAllowAppImpressions: false, Init: function( accountid, bForceRefresh, strOS, preferences, strCC, optsIn ) { var opts = $J.extend( { bNoDefaultDescriptors: false }, optsIn || {} ); var rgDesiredOSTypes = strOS ? strOS.split(',') : 'any'; for( var i=0; i < rgDesiredOSTypes.length; i++ ) { switch( rgDesiredOSTypes[i] ) { case 'mac': GDynamicStore.s_bUserOnMacOS = true; break; case 'linux': GDynamicStore.s_bUserOnLinux = true; break; default: case 'win': GDynamicStore.s_bUserOnWindows = true; break; } } GDynamicStore.s_preferences = preferences || {}; var fnRunOnLoadCallbacks = function() { GDynamicStore.m_bLoadComplete = true; GDynamicStore.InitAppearHandler(); for ( var i = 0; i < GDynamicStore.s_rgfnOnReadyCallbacks.length; i++ ) { try { GDynamicStore.s_rgfnOnReadyCallbacks[i](); } catch ( e ) { console.error( e ); } } GDynamicStore.s_rgfnOnReadyCallbacks = null; GDynamicStore.DecorateDynamicItems(); GDynamicStore.PopulateRecommendedTagList(); }; try { this.RemoveSNRFromURL(); this.RemoveUTMFromURL(); } catch ( e ) { } // Create a new monitor to track impressions this.s_ImpressionTracker = new CAppearMonitor( function( elElement ){ var fnTrack = function( el ) { var $Elem = $J(el); if ( $Elem.data( 'trackedForImpressions' ) ) { return; } $Elem.data( 'trackedForImpressions', true ); // must have appids var strAppIDs = $Elem.data('dsAppid'); if ( !strAppIDs || strAppIDs.length == 0 ) { return; } var snr = GetElemSNR( $Elem ); if ( !snr ) { return; } GDynamicStore.AddImpressionFromDynamicItem( $Elem ); }; fnTrack( elElement ); // Also track sub-elements, which may exist inside our container. This is useful for nested clusters etc. $J(elElement).find("*[data-ds-appid]").each(function(i, j){ fnTrack(j); }); } ); if ( accountid ) { if ( bForceRefresh ) GDynamicStore.InvalidateCache(); var url = 'https://store.steampowered.com/dynamicstore/userdata/?id=' + accountid + '&cc=' + strCC; var unUserdataVersion = WebStorage.GetLocal( 'unUserdataVersion' ); if ( unUserdataVersion ) url += '&v=' + parseInt( unUserdataVersion ); $J.get( url ).done( function( data ) { var fnEnsureObject = function ( rgMaybeArray ) { return ( !rgMaybeArray || ( typeof rgMaybeArray.length != 'undefined' && rgMaybeArray.length == 0 ) ) ? {} : rgMaybeArray; }; var fnConvertToMap = function ( rgData ) { var out = {}; if ( rgData && rgData.length ) { for ( var i = 0; i < rgData.length; i++ ) out[ rgData[i] ] = true; } return out; }; GDynamicStore.s_rgWishlist = fnConvertToMap( data.rgWishlist ); GDynamicStore.s_rgOwnedPackages = fnConvertToMap( data.rgOwnedPackages ); GDynamicStore.s_rgOwnedApps = fnConvertToMap( data.rgOwnedApps ); GDynamicStore.s_rgMasterSubApps = fnConvertToMap( data.rgMasterSubApps ); GDynamicStore.s_rgAutoGrantApps = fnConvertToMap( data.rgAutoGrantApps ); GDynamicStore.s_rgPackagesInCart = fnConvertToMap( data.rgPackagesInCart ); GDynamicStore.s_rgAppsInCart = fnConvertToMap( data.rgAppsInCart ); GDynamicStore.s_rgRecommendedTags = data.rgRecommendedTags || []; GDynamicStore.s_rgIgnoredApps = data.rgIgnoredApps || {} GDynamicStore.s_rgIgnoredPackages = data.rgIgnoredPackages || {}; GDynamicStore.s_rgCurators = data.rgCurators || {}; GDynamicStore.s_rgCurations = data.rgCurations || {}; GDynamicStore.s_rgCreatorsFollowed = fnConvertToMap( data.rgCreatorsFollowed ); GDynamicStore.s_rgCreatorsIgnored = fnConvertToMap( data.rgCreatorsIgnored ); GDynamicStore.s_bAllowAppImpressions = data.bAllowAppImpressions || false; if ( data.rgExcludedTags && data.rgExcludedTags.length > 0 ) { for ( var i = i = 0; i < data.rgExcludedTags.length; ++i ) { var tag = data.rgExcludedTags[i]; GDynamicStore.s_rgExcludedTags[tag.tagid] = tag.name; } } if ( data.rgExcludedContentDescriptorIDs && data.rgExcludedContentDescriptorIDs.length > 0 && !V_GetCookie( 'wants_mature_content') ) { for ( var i = i = 0; i < data.rgExcludedContentDescriptorIDs.length; ++i ) { var id = data.rgExcludedContentDescriptorIDs[i]; GDynamicStore.s_rgExcludedDescIDs[id] = id; } } GDynamicStore.s_nRemainingCartDiscount = data.nRemainingCartDiscount ? data.nRemainingCartDiscount : 0; GDynamicStore.s_nTotalCartDiscount = data.nTotalCartDiscount ? data.nTotalCartDiscount : 0; GDynamicStore.s_rgRecommendedApps = data.rgRecommendedApps || []; GDynamicStore.s_nPromotionalDiscount = data.nPromotionalDiscount ? data.nPromotionalDiscount : 0; GDynamicStore.s_nPromotionalDiscountMinCartAmount = data.nPromotionalDiscountMinCartAmount ? data.nPromotionalDiscountMinCartAmount : 0; GDynamicStore.s_nPromotionalDiscountAvailableUseCount = data.nPromotionalDiscountAvailableUseCount ? data.nPromotionalDiscountAvailableUseCount : 0; }).always( function() { $J(fnRunOnLoadCallbacks); } ); } else { if ( !opts.bNoDefaultDescriptors ) GDynamicStore.s_rgExcludedDescIDs[3] = 3; var url = 'https://store.steampowered.com/dynamicstore/saledata/?cc=' + strCC; $J.get( url ).done( function( data ) { GDynamicStore.s_nPromotionalDiscount = data.nPromotionalDiscount ? data.nPromotionalDiscount : 0; GDynamicStore.s_nPromotionalDiscountMinCartAmount = data.nPromotionalDiscountMinCartAmount ? data.nPromotionalDiscountMinCartAmount : 0; GDynamicStore.s_nPromotionalDiscountAvailableUseCount = data.nPromotionalDiscountAvailableUseCount ? data.nPromotionalDiscountAvailableUseCount : 0; GDynamicStore.s_bAllowAppImpressions = data.bAllowAppImpressions || false; }).always( function() { $J(fnRunOnLoadCallbacks); } ); } }, RemoveSNRFromURL: function() { if ( !window.history || !window.history.replaceState || !window.location.search ) return; GDynamicStore.RemoveParamFromURL( 'snr' ); GDynamicStore.RemoveParamFromURL( 'ser' ); }, RemoveParamFromURL: function( strParamName ) { // find snr param var strParamPrefix = '' + strParamName + '='; var strSearch = window.location.search; if ( strSearch.indexOf( '?' ) == 0 ) strSearch = strSearch.slice( 1 ); var rgParams = strSearch.split( '&' ); var iParam = -1; for ( var i = 0; i < rgParams.length; i++ ) { var strParam = rgParams[i]; if (strParam.indexOf( strParamPrefix ) == 0) { iParam = i; break; } } if ( iParam < 0 ) return; var strRemove = ''; if ( rgParams.length == 1 || (rgParams.length == 2 && rgParams[1].length == 0) ) { // remove the entire search.. just SNR strRemove = '?' + strSearch; } else if ( iParam == 0 ) { // first param of multiple. Remove snr and trailing & strRemove = rgParams[iParam] + '&'; } else { // 2nd+ param of multiple. Remove snr and preceeding & strRemove = '&' + rgParams[iParam]; } if ( strRemove.length > 0 ) { var strNewURL = window.location.href.replace( strRemove, '' ); window.history.replaceState( history.state, null, strNewURL ); } }, RemoveUTMFromURL: function() { if ( !window.history || !window.history.replaceState || !window.location.search ) return; var strSearch = window.location.search; if ( strSearch.indexOf( '?' ) == 0 ) strSearch = strSearch.slice( 1 ); var rgParams = strSearch.split( '&' ); var rgNewParams = []; for ( var i = 0; i < rgParams.length; ++i ) { var strParam = rgParams[i]; if ( strParam.indexOf( 'utm_') == -1 ) { rgNewParams.push( strParam ); } } if ( rgParams.length != rgNewParams.length ) { var strNewURL = window.location.href.replace( window.location.search, '' ); for ( var i = 0; i < rgNewParams.length; ++i ) { var strParam = rgNewParams[i]; strNewURL += ( i == 0 ? '?' : '&' ) + strParam; } window.history.replaceState( history.state, null, strNewURL ); } }, // Fixup name portion of URL via history API, if support and if name portion is incorrect FixupNamePortion: function() { var rel = $J( "link[rel='canonical']" ); if ( rel.length && window.history ) { // have rel=canonical URL and access to history API. // parse out href portion of navigated URL and see if it's OK var detachedAnchor = document.createElement( 'a' ); detachedAnchor.href = rel.attr( "href" ); if ( window.location.pathname != detachedAnchor.pathname ) { // URL portion does not match canonical URL; rewrite it, preserving query params and hash window.history.replaceState( null, null, rel[0].href + window.location.search + window.location.hash ); } } }, s_strAppearSelector: '[data-ds-appid], [data-ds-packageid]', InitAppearHandler: function() { $J(GDynamicStore.s_strAppearSelector).each(function(i, elTarget ){ var $Elem = $J(elTarget); // these are handled manually, so don't add the impression here if ( $Elem.hasClass( 'cluster_capsule' ) || $Elem.hasClass( 'carousel_cap') ) { return; } GDynamicStore.s_ImpressionTracker.RegisterElement( elTarget ) }); // find our horizontal scrollers and add tracking to them $J('.store_horizontal_autoslider' ).each(function(i, elTarget ){ GDynamicStore.s_ImpressionTracker.RegisterScrollEvent( elTarget ); }); GDynamicStore.s_ImpressionTracker.CheckVisibility(); }, s_oImpressionsTracked: {}, AddImpressionFromDynamicItem: function( $Elem ) { if ( !GDynamicStore.s_bAllowAppImpressions ) { return; } if ( $Elem.hasClass( 'app_impression_tracked' ) ) { return; } $Elem.addClass( 'app_impression_tracked' ); var strImpressions = V_GetDecodedCookie( "app_impressions" ); var rgImpressions = strImpressions && strImpressions.length != 0 ? strImpressions.split( "|" ) : []; // commas not allowed in cookie value var strAppIDs = $Elem.data('dsAppid'); if ( !strAppIDs ) { return; } var rgAppIds; if ( typeof strAppIDs == 'string' && ( strAppIDs.indexOf( ',' ) >= 0 || strAppIDs.indexOf( ':' ) >= 0 )) { rgAppIds = strAppIDs.split( /[,:]/ ); } else { rgAppIds = [ parseInt( strAppIDs ) ]; } var snr = GetElemSNR( $Elem ); if ( !snr ) { return; } var rgAppIDsToReport = []; for ( var i = 0; i < rgAppIds.length; i++ ) { var nAppID = rgAppIds[i]; var strImpressionData = nAppID + '@' + snr; if ( !GDynamicStore.s_oImpressionsTracked[ strImpressionData ] ) { GDynamicStore.s_oImpressionsTracked[ strImpressionData ] = true; rgAppIDsToReport.push( nAppID ); } } if ( !rgAppIDsToReport.length ) return; rgImpressions.push( rgAppIDsToReport.join( ':' ) + '@' + snr ); V_SetCookie( "app_impressions", JoinImpressionsUpToLimit( rgImpressions ) ); }, AddImpression: function( $Elem, appID, strLink ) { if ( $Elem.hasClass( 'app_impression_tracked' ) ) { return; } $Elem.addClass( 'app_impression_tracked' ); var navinfo = strLink.match( /[\?&]snr=([^]*)(&|$|#)/ ); if ( navinfo ) { var snr = navinfo[1]; var strImpressions = V_GetCookie( "app_impressions" ); var rgImpressions = strImpressions && strImpressions.length != 0 ? strImpressions.split( "|" ) : []; var strImpressionData = appID + '@' + snr; rgImpressions.push( strImpressionData ); V_SetCookie( "app_impressions", JoinImpressionsUpToLimit( rgImpressions ) ); } }, MarkAppDisplayed: function( rgDisplayList, cItemsToMark ) { // jquery map takes care of arrays as well as array-ish GDynamicStore.MarkAppIDsAsDisplayed( $J.map( rgDisplayList, function ( item ) { return item.appid; } ), cItemsToMark ); }, MarkItemsAsDisplayed: function( rgItems, cItemsToMark ) { for ( var i = 0; i < rgItems.length; i++ ) { const item = rgItems[i]; if ( item.appid ) { GDynamicStore.s_rgDisplayedApps.push( item.appid ); // if this appid is a demo, also mark the parent app as displayed var rgAppData = GStoreItemData.rgAppData[ item.appid ]; if ( rgAppData && rgAppData.demo_for_app ) GDynamicStore.s_rgDisplayedApps.push( rgAppData.demo_for_app ); } else if ( item.bundleid ) { GDynamicStore.s_rgDisplayedBundles.push( item.bundleid ); } else if ( item.packageid ) { GDynamicStore.s_rgDisplayedPackages.push( item.packageid ); } if ( cItemsToMark !== undefined && --cItemsToMark == 0 ) break; } }, MarkAppIDsAsDisplayed: function( rgAppIDs, cItemsToMark ) { for ( var i = 0; i < rgAppIDs.length; i++ ) { if ( rgAppIDs[i] ) { GDynamicStore.s_rgDisplayedApps.push( rgAppIDs[i] ); // if this appid is a demo, also mark the parent app as displayed var rgAppData = GStoreItemData.rgAppData[ rgAppIDs[i] ]; if ( rgAppData && rgAppData.demo_for_app ) GDynamicStore.s_rgDisplayedApps.push( rgAppData.demo_for_app ); if ( cItemsToMark !== undefined && --cItemsToMark == 0 ) break; } } }, HandleClusterChange: function( cluster ) { GDynamicStore.s_ImpressionTracker.CheckVisibility(); var $ScrollingContainer = $J( cluster.elScrollArea ); var capsules = $ScrollingContainer.find( '.cluster_capsule' ); GDynamicStore.s_ImpressionTracker.TrackAppearanceIfVisible( capsules[cluster.nCurCap] ); }, HandleCarouselChange: function( targetid, curPos, pageSize ) { var $ScrollingContainer = $J( "#" + targetid ); var capsules = $ScrollingContainer.find( '.recommendation_carousel_item' ); var idx = (curPos * pageSize); if ( capsules.length != 0 && idx < capsules.length ) { GDynamicStore.s_ImpressionTracker.TrackAppearanceIfVisible( capsules[idx] ); } }, OnReady: function( fnCallback ) { if ( GDynamicStore.m_bLoadComplete ) fnCallback(); else GDynamicStore.s_rgfnOnReadyCallbacks.push( fnCallback ); }, DecorateDynamicItems: function( $Selector, bForceRecalculate ) { if ( !GDynamicStore.m_bLoadComplete ) { GDynamicStore.OnReady( function() { GDynamicStore.DecorateDynamicItems( $Selector ) } ); return; } // locate elements with dynamic store data var strSelector = '[data-ds-appid], [data-ds-packageid], [data-ds-bundleid]'; // update prices for cart if ( GDynamicStore.s_nRemainingCartDiscount != 'undefined ') { UpdatePricesForAdditionalCartDiscount($Selector, GDynamicStore.s_nRemainingCartDiscount); } var bBannerShown = false; if ( GDynamicStore.s_nTotalCartDiscount != 'undefined ') { bBannerShown = UpdateStoreBannerForAdditionalCartDiscount( GDynamicStore.s_nTotalCartDiscount ); } if ( !bBannerShown && GDynamicStore.s_nPromotionalDiscount != 'undefined' ) { bBannerShown = UpdateStoreBannerForPromotionalDiscount( GDynamicStore.s_nPromotionalDiscount, GDynamicStore.s_nPromotionalDiscountMinCartAmount, GDynamicStore.s_nPromotionalDiscountAvailableUseCount ); } var $DynamicElements; if ( $Selector ) { if ( $Selector.is( strSelector ) ) { $DynamicElements = $Selector; } else { $DynamicElements = $Selector.find( strSelector ); } } else { $DynamicElements = $J(strSelector); } $DynamicElements.each( function() { var $El = $J(this); if ( bForceRecalculate ) { $El.removeClass( 'ds_flagged ds_owned ds_wishlist ds_incart' ).children( '.ds_flag' ).remove(); } else if ( $El.data('dsInstrumented') ) { return; } $El.data('dsInstrumented', true); var bOwned = false; var bWanted = false; var bInCart = false; var bIgnored = false; var unBundleID = $El.data('dsBundleid'); var unPackageID = $El.data('dsPackageid'); var strAppIDs = $El.data('dsAppid'); var eSteamDeckCompatCategory = $El.data('dsSteamDeckCompatCategory'); if ( eSteamDeckCompatCategory !== undefined && !$El.data( 'dsSteamDeckCompatHandled' ) ) { $El.data('dsSteamDeckCompatHandled', true); var strClasses = 'ds_steam_deck_compat '; switch( eSteamDeckCompatCategory ) { case 3: strClasses += 'verified'; break; case 2: strClasses += 'playable'; break; case 1: strClasses += 'unsupported'; break; case 0: default: strClasses += 'unknown'; break; } var elSteamDeckCompatCategory = $J( '
', { class: strClasses } ); $El.append( elSteamDeckCompatCategory ); } if ( unBundleID ) { var Bundle = GDynamicStore.GetPersonalizedBundleData( unBundleID, $El.data('dsBundleData') ); if ( !Bundle ) // no data available return; if ( Bundle.m_cUserItemsInBundle == 0 ) { bOwned = true; } else { // pull out all the appids and let the strAppIDs code below handle it var rgAllAppIDsInBundle = []; for( var iBundleItem = 0; iBundleItem < Bundle.m_rgBundleItems.length; iBundleItem++ ) { var BundleItem = Bundle.m_rgBundleItems[iBundleItem]; for ( var iApp = 0; iApp < BundleItem.m_rgIncludedAppIDs.length; iApp++ ) { rgAllAppIDsInBundle.push( BundleItem.m_rgIncludedAppIDs[iApp] ); } } strAppIDs = rgAllAppIDsInBundle.join(','); } GDynamicStore.UpdateDynamicBundleElements( Bundle, $El ); } else if ( unPackageID ) { if ( GDynamicStore.s_rgPackagesInCart[unPackageID] ) bInCart = true; else if ( GDynamicStore.s_rgOwnedPackages[unPackageID] ) bOwned = true; else if ( unPackageID in GDynamicStore.s_rgIgnoredPackages ) bIgnored = true; } if ( strAppIDs && typeof strAppIDs == 'string' && strAppIDs.indexOf( ',' ) >= 0 ) { var rgAppIDs = strAppIDs.split( ',' ); var bValid = false; var bAllOwned = true, bAllWanted = true, bAllInCart = true; for ( var i = 0; i < rgAppIDs.length; i++ ) { var unAppID = parseInt( rgAppIDs[i] ); if ( !unAppID ) continue; bValid = true; if ( !GDynamicStore.s_rgOwnedApps[unAppID] ) bAllOwned = false; if ( !GDynamicStore.s_rgWishlist[unAppID] ) bAllWanted = false; if ( !GDynamicStore.s_rgAppsInCart[unAppID] ) bAllInCart = false; } if ( bValid ) { if ( bAllInCart ) bInCart = bAllInCart; else if ( bAllOwned ) bOwned = bAllOwned; else if ( bAllWanted ) bWanted = bAllWanted; GDynamicStore.s_ImpressionTracker.RegisterElement( this ); } } else if ( parseInt( strAppIDs ) ) { // simple case of single appid var unAppID = parseInt( strAppIDs ); if ( GDynamicStore.s_rgAppsInCart[unAppID] ) bInCart = true; else if ( GDynamicStore.s_rgOwnedApps[unAppID] ) bOwned = true; else if ( GDynamicStore.s_rgWishlist[unAppID] ) bWanted = true; else if ( unAppID in GDynamicStore.s_rgIgnoredApps ) bIgnored = true; GDynamicStore.s_ImpressionTracker.RegisterElement( this ); } var rgExcludedTagNames = GDynamicStore.GetExcludedTagsOverlap( $El ); var rgExcludedContentDescriptorIDs = GDynamicStore.GetExcludedContentDescriptorOverlap( $El ); var rgExcludedCreatorIDs = GDynamicStore.GetExcludedCreatorOverlap( $El ); if ( !$El.hasClass('ds_no_flags') ) { // owned and wishlist are mutually exclusive if ( bOwned ) { $El.addClass( 'ds_flagged ds_owned' ); $El.append( '