Steam Badger

Presents more useful info in Steam UI

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Steam Badger
// @namespace   capitalw_steam
// @description Presents more useful info in Steam UI
// @include		http://steamcommunity.com/id/*/badges/*
// @include     http://steamcommunity.com/profiles/*/badges/*
// @include     http://steamcommunity.com/id/*/games*?tab=all*
// @include		http://steamcommunity.com/profiles/*/games*?tab=all*
// @include     http://steamcommunity.com/id/*/games*?tab=all&games_in_common=1
// @include		http://steamcommunity.com/profiles/*/games*?tab=all&games_in_common=1
// @include     http://steamcommunity.com/id/*/wishlist*
// @include		http://steamcommunity.com/profiles/*/wishlist*
// @include		http://steamcommunity.com/id/*/inventory/*
// @include		http://steamcommunity.com/profiles/*/inventory/*
// @include		http://steamcommunity.com/groups/*
// @include		http://steamcommunity.com/id/*/friends/
// @include		http://steamcommunity.com/profiles/*/friends/
// @include		http://steamcommunity.com/id/*/friendsthatplay/*
// @include		http://steamcommunity.com/profiles/*/friendsthatplay/*
// @include		http://store.steampowered.com/app/*
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js
// @require		http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js
// @version     1.0.18
// @grant       unsafeWindow
// @grant       GM_getValue
// @grant       GM_setValue
// @grant 		GM_getResourceText
// @resource	seedtag http://pastebin.com/raw.php?i=s7wFWwDS
// ==/UserScript==

var badgeGames = JSON.parse(GM_getValue("badgeGames", "[]"))
var installedGames = JSON.parse(GM_getValue("installedGames", "[]"))

var profilesData = JSON.parse(GM_getValue("profilesData", "{}"))
var giftInventoryData = JSON.parse(GM_getValue("giftInventoryData", "{}"))
var giftPrey = JSON.parse(GM_getValue("giftPrey", '[]'))

var preyImage = '<div class="preyDiv"></div>'

var badgesInstalled
var uninstalledAreVisible = true

var unresolvedConnections = 0
var MAX_CONNECTIONS = 4
var pendingAjaxCalls = []
var ajaxIntervalID = null
var AJAX_DELAY = 2000

var GRAB_INVENTORY_DELAY = 1500
var MAX_INVENTORY_TRIES = 10

var untaggedGameIDs = null
var gameTags = JSON.parse(GM_getValue("gameTags", "{}"))
var gamesToNotTag = JSON.parse(GM_getValue("gamesToNotTag", "[]"))
var gameRedirectedFrom = JSON.parse(GM_getValue("gameRedirectedFrom", "{}"))

var gamesLackingTagsOnPage = JSON.parse(GM_getValue("gamesLackingTagsOnPage", "[]"))

var filterGameTags = null

//Only games found with MMO (but not Massively Multiplayer) are 42890, 107900, 212180, 239220, 31740. None of them are MMOs.
//13630 is amazing. DLC with a messed up breadcrumb and no tags/genre.
//This array is sorted for binary search
var TAGS_TO_IGNORE = ["Early Access", "Includes Source SDK", "MMO", "Mods (require HL2)", "New releases"]
var TAGS_TO_TRASH_GAME = ["Downloadable Content", "Game demo"]
var LOAD_SEED_TAGS_THRESHOLD = 56


//If both 237621(batman DLC) and the puzzling 33340 (Dawn of Discover: Venice addon) both require the base game we can trash anything with the Downloadable Content tag

//214490 is  Alien: Isolation, which right now has  no genre or tags. Awkward.

Array.prototype.binarySearch = function(target) {
  var low = 0, high = this.length - 1, i, comparison
  while (low <= high) {
    i = Math.floor((low + high) / 2);
    if (this[i] < target) { low = i + 1; continue; }
    if (this[i] > target) { high = i - 1; continue; }
    return i
  }
  return -1
}

Array.prototype.numericSort = function(){
	this.sort(function(a,b){return a-b})
}


function getTextForResourceFile(){
	var result =  "gameTags=" + JSON.stringify(gameTags)
	result += "\n\rgamesToNotTag="+JSON.stringify(gamesToNotTag)
	result += "\n\rgameRedirectedFrom="+JSON.stringify(gameRedirectedFrom)
	return result
}

//console.info(getTextForResourceFile())


function main(){
	loadSeedTagging()
    var lastPath = getLastPath(window.location.pathname)
    var appID = getAppIDFromURL(window.location.pathname)
    if (appID) {
        if (window.location.pathname.indexOf("friendsthatplay") != -1) {
	       var owners = getFriendOwners(appID)
            addFriendsWhoOwnUI(appID)
            addFriendsWhoLackUI(appID)
            return
	   }
        else if (window.location.pathname.indexOf("app") != -1) {
            window.addEventListener("load", function(){ handleAppPage(appID) }, false)
        	addScanForTagsButton()
            return
        }
    }

    if (isMyAccount()) {
        if (lastPath == "badges") {
            scanForRemaining()
            addTrimmerButton()
        }
        else if (lastPath == "friends") {
            handleFriendsListPage()
        }
        else if (lastPath == "games") {
        	installMutationObserver( handlePersonalGames )
        }
        else if (lastPath == "inventory") {
            window.addEventListener("load", window.setTimeout(function() { scanGiftInventoryData(1) }, 0), false)
        }
        else if (window.location.hash == "#members") {
			//addScanMembersButton()
        }
	}
	else {
		if (lastPath == "games") {
			installMutationObserver(function(){ scanProfileGamesOwned(); addTagFilter();})
            addTagFilter() //For loading smaller lists on Chrome
		}
		else if (lastPath == "wishlist") {
            //console.info("On a wishlist")
            scanProfileGamesWishlist()
            showGiftCountsForWishlist()
            //installMutationObserver( function(){ scanProfileGamesWishlist(); showGiftCountsForWishlist()} )
            // showGiftCountsForWishlist()
            //installMutationObserver( showGiftCountsForWishlist )
		}
	}
}


function loadSeedTagging(){
	if (Object.keys(gameRedirectedFrom).length < LOAD_SEED_TAGS_THRESHOLD){
		var seedTagsSource = GM_getResourceText( "seedtag" )
		baseUntaggable = loadVariableFromSource('gamesToNotTag', seedTagsSource)
		baseTags = loadVariableFromSource('gameTags', seedTagsSource)
		baseRedirects = loadVariableFromSource('gameRedirectedFrom', seedTagsSource)
		if (baseUntaggable) {
			gamesToNotTag = baseUntaggable
			GM_setValue("gamesToNotTag", JSON.stringify(gamesToNotTag))
		}
		if (baseTags) {
			gameTags = baseTags
			GM_setValue("gameTags", JSON.stringify(gameTags))
		}
		if (baseRedirects) {
			gameRedirectedFrom = baseRedirects
			GM_setValue("gameRedirectedFrom", JSON.stringify(gameRedirectedFrom))
		}
	}
}


function loadVariableFromSource(varName, source){
	try {
		var loaded = source.match(RegExp("^"+varName+"=(.*)$", "m"))
		return JSON.parse(loaded[1])
	}
	catch (err){
		return null
	}
}


function getLastPath(fullPath){
	return fullPath.match(".+\/([^\/]+)")[1]
}


function isMyAccount(){
   var userid = $("a.username").attr("href").match("\/(id|profiles)\/(.+)\/(.*)\/$")
   
   if (userid == null) {
     return false
   }
   var viewingid = $("span.profile_small_header_name > a.whiteLink").attr("href").match("\/(id|profiles)\/(.+)$")
   return (viewingid && userid[2] == viewingid[2])
}


function getSteamIDFromPage(root){
	var result = $(root).find("span.profile_small_header_name a.whiteLink")
	if (result.length > 0) {
		result = result.attr("href").match("\/profiles\/([0-9]+)$")
	}
	if (result) { return result[1] }
	result = getSteamIDByName(getProfileNameFromPage(root))
	if (result) { return result }
	result = $('body script[type="text/javascript"]:eq(2)')
	if (result.length > 0) {
		result = result.html().match(/g_steamID = "(\d*)";$/m)
		if (result) { return result[1] }
	}
	return null
}


function getProfileNameFromPage(root){
	return $(root).find("span.profile_small_header_name").text()
}


function getSteamIDByName(name){
	for (var steamID in profilesData){
		if (name == profilesData[steamID].name) {
			return steamID
		}
	}
	return null
}


function scanForRemaining(){
	var gameRows = $("div.badge_title_row")
	badgeGames = new Array()
	
	var appName
	var appID = 0
	
	gameRows.each( function(){
		if ($(this).find("div.badge_title_stats:contains('remaining'):not(:contains('No'))").length == 1) {
			appID = $(this).find('div.badge_title_stats div.badge_title_playgame a').attr("href").match(/[0-9]*$/)[0]
			appName = $(this).find("div.badge_title").contents().filter(function() {
  			  return this.nodeType === 3; //Node.TEXT_NODE
 			  }).text().trim()
			badgeGames.push([appID, appName])
		}
	})
	
    
	GM_setValue("badgeGames", JSON.stringify(badgeGames))
	badgesInstalled = getBadgeGamesInstalled()
}


function installMutationObserver(func){
    var observer = new MutationObserver(checkInsertForInstallResults)
    observer.eventualCallback = func
    observer.observe( document.getElementById("games_list_row_container"), { childList: true, subtree: true })
}


function checkInsertForInstallResults(mutations, observer){
    for (var i =0; i< mutations.length;i++){
        var mutant = mutations[i]
        //added wishlist_items part
        //console.info(mutant.target.className)
        if  (mutant.addedNodes && mutant.addedNodes[0] && 
           (mutant.target.id=="games_list_rows") ){
              window.setTimeout(observer.eventualCallback, 50)
              break
        }
    }
}


function handlePersonalGames(){
	scanForInstalledAndUpdateOwned()
	addTagFilter()
}


function scanForInstalledAndUpdateOwned(){
	if (!isListingAllGames($("body"))){
		return
	}
	installedGames  = new Array()
	var gameRows = $("div.gameListRow")
	var appID = 0
	var steamID = getSteamIDFromPage($("body"))
	
	var allGames = []
	gameRows.each( function (){
		appID = Number($(this).find('a').attr("href").match("[0-9]*$")[0])
		allGames.push(appID)
		if ( $(this).find('div.clientConnItemTextBlock:contains("Ready to play")').length > 0) {
			installedGames.push(appID)
		}
	})
	
	if (steamID  && (allGames.length > 0)){
		allGames.sort( function(a,b){return a-b})
		var storeUpdate = {}
		storeUpdate.owned = allGames
		storeUpdate.name = getProfileNameFromPage($("body"))
		updateOneProfile(steamID, storeUpdate)
	}
	
	GM_setValue("installedGames", JSON.stringify(installedGames))
}


function isListingAllGames(root){
	try {
		var matched = $(root).find("div.info.scroll_info").html().match(/1 - (\d+) of (\d+) items/)
		if (matched[1] == matched[2]){ return true}
	} catch(err){
	}
	return false
}


function addTagFilter(){
	filterGameTags = rebuildFilterGameTags()
	$("select#tagFilter").remove()
	$("span#tagFilterLabel").remove()
	var options = $.map(Object.keys(filterGameTags).sort(), function(tag){
		return ('<option value="'+tag+'"'+((filterGameTags[tag].length == 0) ? ' disabled="disabled"':'a="b"') +'>'+tag+' ('+filterGameTags[tag].length+')</option>')
	})
	$("div#gameslist_sort_options").append('<span id="tagFilterLabel">Require tag:</span><select multiple size="6" id="tagFilter">' +options.join('')+'</select>')
	$("select#tagFilter").change(filterByTags)
	$('body').prepend('<style> div.gameListRow.hidden { display:none}</style>')
}


function getHeuristicText(steamID){
	//We assume the basic gameTags represent steam as a whole in terms of tag distribution (a lie)
	//Then for each SingleTag, we do: (SteamSingleTag*x/SteamAllTags) = (UserSingleTag/UserAllTags)
	// x = (UserSingleTag/UserAllTags)*(SteamAllTags/SteamSingleTag) = (UserSingleTag*SteamAllTags)/(UserAllTags*SteamSingleTag)
	//Also do some screening for really low-incidence tags. Regardless of how many VR-enabled games you have it means nothing.
	var tagMultipliers = {}
	var userTotal = 0
	var steamTotal = 0
	var profile = profilesData[steamID]
	var tag
	var MINIMUM_GAME_CUTOFF = 20
	var LIKE_THRESHOLD = 1.5
	var DISLIKE_THRESHOLD = 0.5
	for (tag in gameTags){
		if (gameTags[tag].length < MINIMUM_GAME_CUTOFF) { continue; }
		steamTotal += gameTags[tag].length
		tagMultipliers[tag] = $.grep(gameTags[tag], function(taggedGame){
			return (profile.owned.binarySearch(taggedGame) != -1)
		}).length
		userTotal += tagMultipliers[tag]
	}
	for (tag in tagMultipliers){
		tagMultipliers[tag] = ((tagMultipliers[tag] *steamTotal)/(userTotal*gameTags[tag].length))
	}
	var sortedTags = Object.keys(tagMultipliers).sort(function(a,b){return tagMultipliers[b] - tagMultipliers[a]})
	var output = ''
	for (var i=0; i < sortedTags.length;i++){
		tag = sortedTags[i]
		if ((tagMultipliers[tag] > LIKE_THRESHOLD) || (tagMultipliers[tag] < DISLIKE_THRESHOLD))
		output += (tag  + ":" + tagMultipliers[tag]) + '\n'
	}
	return output
}


function rebuildFilterGameTags(){
	filterGameTags = {}
	gamesOnPage = []
	$("div#games_list_rows div.gameListRow div.gameLogo a").each(function(){
		gamesOnPage.push(Number($(this).attr("href").match("[0-9]*$")[0]))
	})
	gamesOnPage.sort( function(a,b){return a-b} )
	for (var  key in gameTags){
		filterGameTags[key] = $.grep(gamesOnPage, function(appID,index){ return gameHasTag(appID, key)})
	}
	return filterGameTags
}


function filterByTags(e){
	var tagArray = []
	$(e.target).find("option:selected").each(function(){
		tagArray.push($(this).attr('value'))
	})
	
	$("div#games_list_rows div.gameListRow").each(function(){
		appID = $(this).find('a').attr("href").match("[0-9]*$")[0]
		if (gameHasAllFilterTags(appID, tagArray)){
			$(this).removeClass("hidden")
		}
		else { $(this).addClass("hidden") }
	})
}


function gameHasAllFilterTags(appID, tagArray){
	for (var i=0; i < tagArray.length; i++){
		if ((gameTags[tagArray[i]].binarySearch(appID) == -1) ) { return false }
	}
	return true
}


function addScanForTagsButton(){
	var extraButton = '<a class="btn_blue_white_innerfade btn_medium ajaxscanbtn" id="scanfortagsbutton"><span>Tag untagged games (' +
		(getUntaggedGameIDs().length - gamesLackingTagsOnPage.length) +')</span></a>'
	$("div.apphub_OtherSiteInfo").children().last().after(extraButton)
	$("#scanfortagsbutton").click(scanGamesForTags)
}


function scanGamesForTags(){
	if (pendingAjaxCalls.length > 0) {
		return
	}
	var uniqueAppIDs = getUntaggedGameIDs()
	console.info(uniqueAppIDs)
	pendingAjaxCalls = []
	for (var i = (uniqueAppIDs.length-1); i>= 0; i--) {
		pendingAjaxCalls.push(getAjaxForLoadAppPage(uniqueAppIDs[i]))
	}
	$("a.ajaxscanbtn span").animate({color: "#FF9966"}, 1000)
	ajaxIntervalID = setInterval(spinOffAjaxRequest, AJAX_DELAY)
}


function getUntaggedGameIDs(){
	if (untaggedGameIDs != null) {
		return untaggedGameIDs
	}
	
	var untaggedSet = {}
	var i, profile, appID
	for (var steamID in profilesData) {
		profile = profilesData[steamID]
        if (profile.owned){
			for (i = 0; i < profile.owned.length; i++){
				appID = profile.owned[i]
				if (!(untaggedSet.hasOwnProperty(appID)) && !(gameHasAnyTag(appID))) {
					untaggedSet[appID] = true
				}
			}
        }
		if (profile.wishlist) {
			for (i = 0; i < profile.wishlist.length; i++){
				appID = profile.wishlist[i]
				if (!(untaggedSet.hasOwnProperty(appID)) && !(gameHasAnyTag(appID))) {
					untaggedSet[appID] = true
				}
			}
		}
	}
	
	for (i = 0; i < gamesToNotTag.length; i++){
		delete untaggedSet[gamesToNotTag[i]]
	}
	untaggedGameIDs = Object.keys(untaggedSet).map(Number)
    untaggedGameIDs.sort()
	
	var cleanedLackingTags = []
	for (var i = 0; i < gamesLackingTagsOnPage.length ;i++){
		appID = gamesLackingTagsOnPage[i]
		if ($.inArray(appID, untaggedGameIDs) != -1){
			cleanedLackingTags.push(appID)
		}
	}
	gamesLackingTagsOnPage = cleanedLackingTags
	GM_setValue("gamesLackingTagsOnPage", JSON.stringify(gamesLackingTagsOnPage))
	
	return untaggedGameIDs
}


function gameHasAnyTag(appID){
	for (var tag in gameTags){
		if (gameHasTag(appID, tag)) { return true }
	}
	return false
}


function getAjaxForLoadAppPage(appID) {
	var link = "http://store.steampowered.com/app/"+appID+"/"
	return {
		url: link,
		crossDomain: true,
		xhrFields: {
         withCredentials: true 
       	},
       	error: function(){
       		//console.info("Tagging: Error loading page for " + appID)
       	},
		success: function(data, textStatus, jqXHR) {
				handleAppPageLoad(data, textStatus, jqXHR, appID) 
			},
		complete: function(jqXHR, textStatus, errorThrown){
			completeAsyncAjax()
		}
	}
}


function handleAppPageLoad(data, textStatus, jqXHR, appID) {
	scanGameTags($(data),appID)
	spotDifferingAppID($(data), appID)
}


function spotDifferingAppID(data, referringAppID){
	var redirectedAppID = getAppIDFromLinkOnPage(data)
	if  (redirectedAppID && (redirectedAppID  != referringAppID)){
		gameRedirectedFrom[redirectedAppID] = referringAppID
		GM_setValue("gameRedirectedFrom", JSON.stringify(gameRedirectedFrom))
	}
}


function getAppIDFromLinkOnPage(data){
	var link =  $(data).filter('link[rel="canonical"]').attr('href')
	if (link) {
		return (getAppIDFromURL($(data).filter('link[rel="canonical"]').attr('href')))
	}
	return null
}


function addTrimmerButton(){
	var extraButton = '<label id="trimmerbutton" class="badge_sort_option whiteLink es_badges"><span>Easy cards only ('+
	   badgesInstalled.length+") "+ "("+installedGames.length+" games installed"+')</span></label>'
	$("div.profile_badges_sortoptions").children().last().after(extraButton)
	$("#trimmerbutton").click(toggleUninstalledVisibility)
}


function toggleUninstalledVisibility(){
	uninstalledAreVisible = !uninstalledAreVisible
	if (uninstalledAreVisible) {
		$("div.badge_row").css("display", "")
		$("#trimmerbutton").text("Easy cards (" +badgesInstalled.length+") "+ " ("+installedGames.length+" games installed)")
	}
	else { hideUninstalled() }
}


function hideUninstalled(){
	var gameRows = $("div.badge_row")	
	var appID = 0

	gameRows.each( function(){
		if ($(this).find("div.badge_title_stats:contains('remaining'):not(:contains('No'))").length == 1) {
			appID = $(this).find('div.badge_title_stats div.badge_title_playgame a').attr("href").match(/[0-9]*$/)[0]
			if (badgesInstalled.indexOf(appID)  == -1) {
				$(this).css("display","none")
			}
		}
		else {
			$(this).css("display","none")
		}
	})
	$("#trimmerbutton").text("Show uninstalled")
}


function getBadgeGamesInstalled(){
	var result = new Array()
	for  (var i = 0; i < badgeGames.length; i++) {
		if (installedGames.indexOf(parseInt(badgeGames[i][0]) ) != -1) {
			result.push(badgeGames[i][0])
		}
	}
	return result
} 


function scanGiftInventoryData(tries){
	if (tries > MAX_INVENTORY_TRIES) {
		return
	}
	
	var steamInventory = grabRgInventory()
	if (null == steamInventory  ){
		window.setTimeout(function() { scanGiftInventoryData(tries+1) }, GRAB_INVENTORY_DELAY)
		return
	}
    var attemptedNewGiftData = getGiftAssociativeArray( getNumbersAndNamesFromRgInventory(steamInventory) )
    if (null == attemptedNewGiftData) {
        window.setTimeout(function() { scanGiftInventoryData(tries+1) }, GRAB_INVENTORY_DELAY)
		return
    }
	giftInventoryData = getGiftAssociativeArray( getNumbersAndNamesFromRgInventory(steamInventory) )
	GM_setValue("giftInventoryData", JSON.stringify(giftInventoryData))
	$("div#context_selector").prepend('<div style="color:green">Gift inventory scanned</div>')
	addGiftRecipientSelector()
}


function getGiftAssociativeArray(giftTuples){
    if (giftTuples.length == 0) {
        return null
    }
	var finalGifts = {}
	var lastAppID = -1
	var tuple
	for (var i=0; i< giftTuples.length; i++){
		tuple = giftTuples[i]
		if (tuple[0] == lastAppID) {
			finalGifts[lastAppID].count += 1
		}
		else {
			lastAppID = tuple[0]
			finalGifts[lastAppID] = {"name":tuple[1], "count":1}
		}
	}
	return finalGifts
}


function addGiftRecipientSelector(){
	var recipientSelect = $('<select id="recipient_selector" />')
	//console.info(recipientSelect)
	$("<option value='-1'>Choose your gift target</option>").appendTo(recipientSelect)
	var sortedArray = []
	for (var entryID in profilesData){
		sortedArray.push([profilesData[entryID]["name"], entryID])
	}
	sortedArray.sort(function(a,b) {return a[0].localeCompare(b[0])})
	for (var i=0; i < sortedArray.length; i++){
		$('<option />', {value: sortedArray[i][1], text: sortedArray[i][0]}).appendTo(recipientSelect)
	}
	$('div#context_selector').append(recipientSelect)
	recipientSelect.change(recipientSelectorChange)
	addExtraFilteringToPageJS()
}


var filterFuncString =  '<script type="text/javascript">\
function getLastPath(fullPath){\
	return fullPath.match(".+\/([^\/]+)")[1]\
}\
\
function getGiftItemAppIDsAndNames(rgItem) {\
	var hrefPattern = ' + /<a href="[^"]+\/app\/(\d+)\/?">([^<]+)</igm +';\
	try {\
		if (rgItem.actions && rgItem.actions[0].link.indexOf("/sub/") == -1) {\
			return [ [getLastPath(rgItem.actions[0].link), rgItem.name] ];\
		}\
		else {\
			var result = [];\
			var alldescs = "";\
			for (var i = 0; i < rgItem.descriptions.length; i++){\
				alldescs = alldescs + rgItem.descriptions[i].value;\
			}\
			var oneHref;\
			while ((oneHref = hrefPattern.exec(alldescs)) != null){\
				result.push(new Array(oneHref[1], oneHref[2]));\
			}\
			return result;\
		};\
	} catch (err) {\
		if (rgItem && rgItem.name){\
			console.info(err);\
		}\
		return null;\
	};\
};\
\
Filter.MatchItemNotOwned = function(elItem, ownedList){\
	if (!elItem || !elItem.rgItem || !ownedList){\
		return true;\
	}\
	var giftGames = getGiftItemAppIDsAndNames(elItem.rgItem);\
	if (giftGames != null) {\
		for (var i = 0; i < giftGames.length; i++){\
			if (ownedList.indexOf(Number(giftGames[i][0])) == -1){\
				return true;\
			};\
		};\
		return false;\
	};\
	return true;\
};\
\
Filter._oldMatchItem = Filter.MatchItem;\
Filter.MatchItem = function( elItem, rgTerm, rgCategories ) {\
	return (Filter._oldMatchItem(elItem, rgTerm, rgCategories) && (Filter.MatchItemNotOwned(elItem, window.alreadyOwnedGames)));\
};\
</script>'

//Section 9 is coming back null for gifts
//My jquery parses hres, but the $J version on the page doesn't. Okay, regex it is!
// Regex needs to match <a href="SOMESTUFF"
// <a href="[^"]">


function addExtraFilteringToPageJS(){
	$("head").append(filterFuncString)
}


function recipientSelectorChange(){
	var selected = $("#recipient_selector").val()
	filterGiftsForRecipient(selected)
}


function filterGiftsForRecipient(recipID){
	var gift
	var ownedArray = []
	
	if ((profilesData[recipID]) && (profilesData[recipID].owned)){
		ownedArray = profilesData[recipID].owned
	}
	
	unsafeWindow.alreadyOwnedGames = cloneInto(ownedArray, unsafeWindow)
	var oldFilterText = $("input#filter_control").val()
	$("input#filter_control").val("|")
	$("input#filter_control").click()
	$("input#filter_control").val(oldFilterText)
	$("input#filter_control").click()
}


function personOwns(person, appID){
	var refAppID = getReferringAppID(appID)
	return (("owned" in person) && ((person.owned.binarySearch(appID) != -1) || (person.owned.binarySearch(refAppID) != -1)) )
}


function grabRgInventory() {
	if ((unsafeWindow.UserYou.rgAppInfo["753"].rgContexts["1"].inventory) && 
		(unsafeWindow.UserYou.rgAppInfo["753"].rgContexts["1"].inventory.rgInventory) ) {
		return unsafeWindow.UserYou.rgAppInfo["753"].rgContexts["1"].inventory
	}
	return null
}


function getNumbersAndNamesFromRgInventory(steamInventory){
	var tidyGifts = []
	var gift
	for (var i=0; i < steamInventory.rgItemElements.length; i++){
			gift = steamInventory.rgItemElements[i].rgItem
			var appIDsAndNames = getGiftItemAppIDsAndNames(gift)
			if (appIDsAndNames) {
				$.merge(tidyGifts, appIDsAndNames)
			}
		}
	tidyGifts.sort(function(a,b) { return a[0]-b[0]})
	return tidyGifts
}


function getGiftItemAppIDsAndNames(rgItem) {
	var hrefPattern = /<a href="[^"]+\/app\/(\d+)\/?">([^<]+)</igm
	try {
		if (rgItem.actions && rgItem.actions[0].link.indexOf("/sub/") == -1 ) {
			return [ [getLastPath(rgItem.actions[0].link), rgItem.name] ]
		}
		else {
			var result = []
			var alldescs = ""
			for (var i = 0; i < rgItem.descriptions.length; i++){
				alldescs = alldescs + rgItem.descriptions[i].value
			}
			var oneHref
			while ((oneHref = hrefPattern.exec(alldescs)) != null){
				result.push(new Array(oneHref[1], oneHref[2]))
			}
			return result
		}
	} catch (err) {
		console.info(err)
		return null
	}
}


function handleFriendsListPage(){
	scanFriendsList()
	addScanEachFriendButton()
	addPreyImages()
}


function scanFriendsList(){
	var friendBlocks = $("div.friendBlock.persona")
	var updatedFriends = {}
	friendBlocks.each( function () {
		var steamID = $(this).find("div.manage_friend_checkbox input.friendCheckbox").attr("data-steamid")
		var avatar = $(this).find("div.playerAvatar img").attr("src")
		var name = $(this).find("span.friendSmallText").parent("div").text().split("\n")[1].trim()
		updatedFriends[steamID.toString()] = {"avatar":avatar,"name":name, "isFriend":true}
	})
	
	updateAllFriendsData(updatedFriends)
}


function addScanEachFriendButton(){
	var extraButton = '<a class="btn_blue_white_innerfade btn_medium ajaxscanbtn" id="scaneachfriendbutton"><span>Scan everyone</span></a>'
	$("div.manage_friends_header").children().last().after(extraButton)
	$("#scaneachfriendbutton").click(scanEachFriend)
}


function addPreyImages(){
	var friendBlocks = $("div.friendBlock.persona")
	friendBlocks.append(preyImage)
	$("div.preyDiv").click(togglePreyStatus)
	friendBlocks.each( function() {
		var steamID = ($(this).find("div.manage_friend_checkbox input.friendCheckbox").attr("data-steamid"))
		if (isPrey(steamID) ) {
			$(this).find('div.preyDiv').addClass("active")
		}
	})
	$('body').prepend('<style> div.preyDiv { position:absolute; right:0; z-index:3; top:8px;}' +
	'div.preyDiv {background-image:url("http://media.steampowered.com/steamcommunity/public/images/apps/200510/b9d2d27c2fc9d188f605b4300e475e8d510c41a4.jpg");'+
		'background-size:32px 32px; padding:32px 32px 0 0; z-index:10;}'+
	'div.preyDiv.active {background-image:url("http://media.steampowered.com/steamcommunity/public/images/apps/200510/c5deca36ee53aab6eebc4e5db9d76625d4c67914.jpg")}</style>')
}


function isPrey(steamID){
	return (giftPrey.indexOf(steamID) != -1)
}


function togglePreyStatus(){
	var steamID = $(this).parent().find("div.manage_friend_checkbox input.friendCheckbox").attr("data-steamid")
	var i = giftPrey.indexOf(steamID)
	if (i != -1){
		giftPrey.splice(i, 1)
		$(this).removeClass("active")
	}
	else {
		giftPrey.push(steamID)
		$(this).addClass("active")
	}
	GM_setValue("giftPrey", JSON.stringify(giftPrey))
}


function scanEachFriend(){
	if (pendingAjaxCalls.length > 0) {
		return
	}
	pendingFriendsProfiles = getFilteredProfiles(function(entry) {
		var now = $.now()
		return (("isFriend" in entry) && (entry.isFriend) 
		&& ((entry.lastAttempted == undefined) || ((now - entry.lastAttempted) > 300000) ) )
		} )
	pendingAjaxCalls = []
	for (var steamID in pendingFriendsProfiles) {
		pendingAjaxCalls.push(getAjaxForLoadOwnedGames(steamID))
		pendingAjaxCalls.push(getAjaxForLoadWishlistGames(steamID))
	}
	
	$("#scaneachfriendbutton span").animate({color: "#FF9966"}, 1000)
	ajaxIntervalID = setInterval(spinOffAjaxRequest, AJAX_DELAY)
}


function spinOffAjaxRequest(){
	if ((0==unresolvedConnections) && (0 == pendingAjaxCalls.length)) {
		clearInterval(ajaxIntervalID)
		ajaxIntervalID = null
		updateConnectionDisplay()
		return
	} 
	while ((unresolvedConnections < MAX_CONNECTIONS) && (pendingAjaxCalls.length != 0)) {
		var nextRequest = pendingAjaxCalls.pop()
		unresolvedConnections++
		$.ajax(nextRequest)
	}
	updateConnectionDisplay()
}


function updateConnectionDisplay(){
	if ((0 == unresolvedConnections) && (0 == pendingAjaxCalls.length)) {
		$("a.ajaxscanbtn span").text("Done scanning")
		$("a.ajaxscanbtn span").animate({color: "#99FF66"}, 1000)
	}
	else {
		$("a.ajaxscanbtn span").text(unresolvedConnections + " active requests, " + 
			pendingAjaxCalls.length + " requests remaining")
	}
}


function getAjaxForLoadOwnedGames(steamID){
	var link = "http://steamcommunity.com/profiles/"+steamID+"/games?tab=all#0|2000"
	return {
		url: link,
		type: "GET",
		ifModified: true,
		beforeSend: function(xhr) {
       		xhr.setRequestHeader(
            	'X-Requested-With',
            	{toString: function() { return ''; }
        	})
        	stampConnectionAttempt(steamID)
        },
        headers: { 
        	"Accept" : "text/plain; charset=utf-8",
        	"Content-Type": "text/plain; charset=utf-8"
    	},
		dataType: "html",
		success: function (data, textStatus, jqXHR) {
				handleOwnedGamesLoad(data, textStatus, jqXHR, steamID) 
			},
		complete: completeAsyncAjax
	}
}


function getAjaxForLoadWishlistGames(steamID){
	var link = "http://steamcommunity.com/profiles/"+steamID+"/wishlist" 
	return {
		url: link,
		ifModified: true,
		success: handleWishlistGamesLoad,
		complete: completeAsyncAjax,
		beforeSend: function(xhr) { stampConnectionAttempt(steamID) }
	}
}


function completeAsyncAjax(jqXHR, textStatus){
	unresolvedConnections--
	updateConnectionDisplay()
}


function handleWishlistGamesLoad(data, textStatus, jqXHR) {
	if (textStatus == "success") {
		scanProfileGamesList($(data), "wishlist", "wishlistRow")
	}
}


function handleOwnedGamesLoad(data, textStatus, jqXHR, steamID) {
	if (textStatus == "success") {
		var gamesList = getAppIDsFromRgGames(getRgGamesFromHTML(data))
		addGamesListToSteamID(steamID, gamesList, "owned")
	}
}


function getRgGamesFromHTML(data){
	var re =  /^<script language="javascript">([\n\s\S.]*)var rgGames =(.*);/gm
    var match = re.exec(data)
    return JSON.parse(match[2])
}


function getAppIDsFromRgGames(rgGames){
	var appIDs = []
	for (var i = 0; i < rgGames.length; i++){
		appIDs.push(rgGames[i]["appid"])
	}
	return appIDs
}


function showGiftCountsForWishlist(){
    var appIDs = getGamesList($("body"), "wishlistRow")
    for (var i = 0; i < appIDs.length; i++){
       var appID = appIDs[i]
       var giftCount = getGiftInventoryCount(appID)
	   if (giftCount > 0) {
	      var textdiv = $("div#game_"+appID +" h4")
          textdiv.css("color", "red")
          var  giftText =  " (" +giftCount + " gift" +((giftCount> 1) ?"s" : "")  +" in inventory)"
	      textdiv.text(textdiv.text() + giftText)
	   }
    }
}


function stampConnectionAttempt(steamID){
	var storeUpdate = {}
	storeUpdate.lastAttempted = $.now()
	updateOneProfile(steamID, storeUpdate)
}


function updateAllFriendsData(updatedFriends){
	var steamID
	for (steamID in profilesData) {
		if (!(steamID in updatedFriends)) {
			profilesData[steamID].isFriend = false
		}
	}
	for(steamID in updatedFriends) {
 		updateOneProfile(steamID, updatedFriends[steamID])
 	}
}


function updateOneProfile(steamID, updated){
	if (steamID in profilesData) {
 		for (var property in updated)
 			profilesData[steamID][property] = updated[property]
 		}
 	else {
 		profilesData[steamID] = updated
 	}
 	GM_setValue("profilesData", JSON.stringify(profilesData))
}


function scanProfileGamesOwned(){
	var root = $("body")
	if (isListingAllGames(root)){
		scanProfileGamesList(root, "owned", "gameListRow")
	}
	else{
		console.info("Failed to list all owned games for: http://steamcommunity.com/profiles/"+steamID)
	}
}


function scanProfileGamesWishlist(){
	scanProfileGamesList($("body"), "wishlist", "wishlistRow")
}


function scanProfileGamesList(root, storageKey, rowClass){
	var steamID = getSteamIDFromPage(root)
	if (!(steamID in profilesData)) {
		return
	}
	addGamesListToSteamID(steamID, getGamesList(root, rowClass), storageKey)
}


function addGamesListToSteamID(steamID, gamesList, storageKey) {
	var storeUpdate = {}
	gamesList.sort(function(a,b){return a - b}) 
	storeUpdate[storageKey] = gamesList
	updateOneProfile(steamID, storeUpdate)
}


function getGamesList(root, rowClass){
	var listedGames = new Array()
	var appID
	$(root).find("div."+rowClass).each( function (){
		appID = $(this).find('a').attr("href").match("[0-9]*$")[0]
		listedGames.push(Number(appID))
	})
	return listedGames
}


function getAppIDFromURL(url){
	var appID = url.match("(app|friendsthatplay)\/([0-9]+)\/?$")
	if (appID != null) {
		return Number(appID[2])
	}
	return null
}


function getFriendOwners(appID){
	var refAppID = getReferringAppID(appID)
	return getFilteredProfiles(function(entry) {return (("owned" in entry) && ((entry.owned.binarySearch(appID) != -1) || (entry.owned.binarySearch(refAppID) != -1)))} )
}


//TODO: This can be a misnomer right now since we don't filter for friends.
function getFriendLackers(appID){
	var refAppID = getReferringAppID(appID)
	return getFilteredProfiles(function(entry) {
		return (("owned" in entry) && (entry.owned.binarySearch(appID) == -1) && (entry.owned.binarySearch(refAppID) == -1))
	} )
}


function getPreyLackers(appID){
	result = getFriendLackers(Number(appID))
	for (steamID in result){
		if (!(isPrey(steamID))) {
			delete(result[steamID])
		}
	}
	return result
}


function getFilteredProfiles(filterFunc){
	var filtered = {}
	for (var steamID in profilesData) {
		if (filterFunc(profilesData[steamID]) == true) {
			filtered[steamID] = profilesData[steamID]
		}
	}
	return filtered
}


function addFriendsWhoOwnUI(appID){
	var owners = getFriendOwners(appID)
	addFriendsUI(appID, "Friends who just own ", owners)
}


function addFriendsWhoLackUI(appID){
	var lackers = getFriendLackers(appID)
	addFriendsUI(appID, "Friends who lack ", lackers)
}


function addFriendsUI(appID, friendDesc, players){
	var gameLink = '<a href="http://steamcommunity.com/app/' +appID + '">'+$("div.friendListSectionHeader a").html() +'</a><span class="underscoreColor">_</span>'
	var sectionHead = '<div class="mainSectionHeader friendListSectionHeader">' + friendDesc + gameLink +'</div>'
	var friendList  = '<div class="profile_friends">'
	
	if (Object.keys(players).length > 0){
		for (var steamID in players) {
			friendList += createFriendBlock(steamID)
		}
		friendList += '</div><div style="clear: left;"></div></div>'
		$("div#memberList").children().last().after(sectionHead)
		$("div#memberList").children().last().after(friendList)
	}
}


function createFriendBlock(steamID){
	var result = '<div class="friendBlock persona"> <a class="friendBlockLinkOverlay" href="http://steamcommunity.com/profiles/'+steamID+
		'"></a><div class="playerAvatar online"><img src="'+profilesData[steamID].avatar+'"></div>'+
		'<div class="friendBlockContent">'+profilesData[steamID].name+'</div></div>'
	return result
}


function handleAppPage(appID){
	showAppGiftingTargets(appID)
	scanGameTags($("body"), appID)
}


function scanGameTags(root,appID){
	if (!onGamePage(root)){
		if ($(root).find("div#supernav").length != 0){
			addToGamesToNotTag(appID)
		}
		return
	}
	var potentialTags = []
	$(root).find('div.game_meta_data div#category_block div.game_area_details_specs a.name, div.game_details a[href^="http://store.steampowered.com/genre/"]').each( function(){
		potentialTags.push($(this).text())
	})
	
	if (potentialTags.length > 0) {
		removeFromGamesLackingTagsOnPage(appID)
		for (var i=0; i < TAGS_TO_TRASH_GAME.length; i++){
			if (potentialTags.indexOf(TAGS_TO_TRASH_GAME[i]) != -1){
				addToGamesToNotTag(appID)
				return
			}
		}
	
		for (var i=0; i < potentialTags.length; i++){
			addAppIDToTag(appID, potentialTags[i])
		}
		GM_setValue("gameTags", JSON.stringify(gameTags))
	}
    
    if (!gameHasAnyTag(appID)){
        addToGamesLackingTagsOnPage(appID)
    }
}


function addToGamesLackingTagsOnPage(appID){
	if (gamesLackingTagsOnPage.indexOf(appID) == -1){
		gamesLackingTagsOnPage.push(appID)
		GM_setValue("gamesLackingTagsOnPage", JSON.stringify(gamesLackingTagsOnPage))
	}
}


function removeFromGamesLackingTagsOnPage(appID){
	var removeIndex = gamesLackingTagsOnPage.indexOf(appID)
	if (removeIndex != -1){
		gamesLackingTagsOnPage.splice(removeIndex,1)
		GM_setValue("gamesLackingTagsOnPage", JSON.stringify(gamesLackingTagsOnPage))
	}
}


function addToGamesToNotTag(appID){
	gamesToNotTag.push(appID)
	gamesToNotTag.sort()
	GM_setValue("gamesToNotTag", JSON.stringify(gamesToNotTag))
}


function onGamePage(root){
	if ($(root).find('div.breadcrumbs div.blockbg a[href="http://store.steampowered.com/search/?term=&snr=1_5_9__205"]').length != 1){
		return false
	}
	if ($(root).find('div.breadcrumbs div.blockbg a[href^="http://store.steampowered.com/dlc/"]').length != 0){
		return false
	}
	if ($(root).find('div.notice_box_content a[href^="http://www.steampowered.com/v/index.php?area=game&AppId="]').length != 0){
		return false
	}
	return true
}


function addAppIDToTag(appID, tagText){
	if (gameTags[tagText]){
		if (gameHasTag(appID, tagText)){ return }
		gameTags[tagText].push(Number(appID))
		gameTags[tagText].sort(function(a,b){return a-b})
	}
	else{
		if (!(TAGS_TO_IGNORE.binarySearch(tagText) != -1)){
			gameTags[tagText] = [Number(appID)]
        }
        else {
            return false
        }
	}
    return true
}


function gameHasTag(appID, tagText){
	return (gameTags[tagText].binarySearch(appID) != -1) 
}


function showAppGiftingTargets(appID){
	var giftCount = getGiftInventoryCount(appID)
	if (giftCount > 0) {
		var  giftText =  " (" +giftCount + " gift" +((giftCount> 1) ?"s" : "")  +" in inventory)"
		$("div.apphub_AppName").first().append(giftText)
	}
	
	var prey = getPreyLackers(appID)
	if  (Object.keys(prey).length > 0){
		var preyDiv = '<div class="preyDiv">'
		$.each(prey, function(key, value) {
				preyDiv += ' <a href="http://steamcommunity.com/profiles/' + key + '"' +
				((value["wishlist"] && (value.wishlist.binarySearch(appID) != -1)) ?" class='wished'" : "")+
				'>' + value.name + '</a>'
			})
		preyDiv += '</div>'
		$("div.apphub_AppName").append(preyDiv)
		$('body').prepend('<style> div.preyDiv {position: absolute; z-index:4;} ' +
			'div.preyDiv a { font-size: 14px; color:#FFFFFF} '+
			'div.preyDiv a.wished{color:#32CD32;font-size:16px; animation: glow .75s infinite alternate;}'+
			'@keyframes glow { to { text-shadow: 0 0 4px #00FF00; } }'+
			'div.preyDiv a[href$="/76561198024962141"]{color:#FFCCFF;}'+
			'div.preyDiv a[href$="/76561198024962141"].wished {color:#FF00FF;animation: pinkglow .75s infinite alternate;}'+
			'@keyframes pinkglow { to { text-shadow: 0 0 4px #FFCCFF; } }' +
			'</style>')
	}
}


function getReferringAppID(appID){
	if (appID in gameRedirectedFrom){
		return gameRedirectedFrom[appID]
	}
	return appID
}


function getGiftInventoryCount(appID){
	return ( (appID in giftInventoryData) ? giftInventoryData[appID].count : 0)
}



main()