// ==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()