Skip to content

Instantly share code, notes, and snippets.

@Haoose
Created September 8, 2017 19:51
Show Gist options
  • Save Haoose/023b8949b3c1f96e01e3062dcdf4e89e to your computer and use it in GitHub Desktop.
Save Haoose/023b8949b3c1f96e01e3062dcdf4e89e to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Steam Economy Enhancer
// @namespace https://github.com/Nuklon
// @author Nuklon
// @license MIT
// @version 5.8.0
// @description Enhances the Steam Inventory and Steam Market.
// @include *://steamcommunity.com/id/*/inventory*
// @include *://steamcommunity.com/profiles/*/inventory*
// @include *://steamcommunity.com/market*
// @include *://steamcommunity.com/tradeoffer*
// @require https://code.jquery.com/jquery-3.2.1.min.js
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @require https://raw.githubusercontent.com/kapetan/jquery-observe/master/jquery-observe.js
// @require https://raw.githubusercontent.com/superRaytin/paginationjs/master/dist/pagination.js
// @require https://raw.githubusercontent.com/caolan/async/master/dist/async.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.4.3/localforage.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/datejs/1.0/date.min.js
// @require https://raw.githubusercontent.com/javve/list.js/v1.5.0/dist/list.min.js
// @require https://github.com/rmariuzzo/checkboxes.js/releases/download/v1.2.0/jquery.checkboxes-1.2.0.min.js
// @homepageURL https://github.com/Nuklon/Steam-Economy-Enhancer
// @supportURL https://github.com/Nuklon/Steam-Economy-Enhancer/issues
// @downloadURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
// @updateURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
// ==/UserScript==
// jQuery is already added by Steam, force no conflict mode.
(function ($, async) {
$.noConflict(true);
const STEAM_INVENTORY_ID = 753;
const PAGE_MARKET = 0;
const PAGE_MARKET_LISTING = 1;
const PAGE_TRADEOFFER = 2;
const PAGE_INVENTORY = 3;
const COLOR_ERROR = '#8A4243';
const COLOR_SUCCESS = '#407736';
const COLOR_PENDING = '#908F44';
const COLOR_PRICE_FAIR = '#496424';
const COLOR_PRICE_CHEAP = '#837433';
const COLOR_PRICE_EXPENSIVE = '#813030';
const ERROR_SUCCESS = null;
const ERROR_FAILED = 1;
const ERROR_DATA = 2;
var marketLists = [];
var totalNumberOfProcessedQueueItems = 0;
var totalNumberOfQueuedItems = 0;
var spinnerBlock =
'<div class="spinner"><div class="rect1"></div>&nbsp;<div class="rect2"></div>&nbsp;<div class="rect3"></div>&nbsp;<div class="rect4"></div>&nbsp;<div class="rect5"></div>&nbsp;</div>';
var numberOfFailedRequests = 0;
var enableConsoleLog = false;
var isLoggedIn = typeof g_rgWalletInfo !== 'undefined' && g_rgWalletInfo != null || (typeof g_bLoggedIn !== 'undefined' && g_bLoggedIn);
var currentPage = window.location.href.includes('.com/market')
? (window.location.href.includes('market/listings')
? PAGE_MARKET_LISTING
: PAGE_MARKET)
: (window.location.href.includes('.com/tradeoffer')
? PAGE_TRADEOFFER
: PAGE_INVENTORY);
var market = new SteamMarket(g_rgAppContextData,
typeof g_strInventoryLoadURL !== 'undefined' && g_strInventoryLoadURL != null
? g_strInventoryLoadURL
: location.protocol + '//steamcommunity.com/my/inventory/json/',
isLoggedIn ? g_rgWalletInfo : undefined);
var currencyId =
isLoggedIn &&
market != null &&
market.walletInfo != null &&
market.walletInfo.wallet_currency != null
? market.walletInfo.wallet_currency
: 3;
var currencySymbol = GetCurrencySymbol(GetCurrencyCode(currencyId));
function SteamMarket(appContext, inventoryUrl, walletInfo) {
this.appContext = appContext;
this.inventoryUrl = inventoryUrl;
this.walletInfo = walletInfo;
this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', '');
if (!this.inventoryUrlBase.endsWith('/'))
this.inventoryUrlBase += '/';
}
//#region Settings
const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE';
const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE';
const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE';
const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE';
const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE';
const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE';
const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET';
const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM';
const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q';
const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE';
const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY';
const SETTING_MARKET_PAGE_COUNT = 'SETTING_MARKET_PAGE_COUNT';
var settingDefaults =
{
SETTING_MIN_NORMAL_PRICE: 0.05,
SETTING_MAX_NORMAL_PRICE: 2.50,
SETTING_MIN_FOIL_PRICE: 0.15,
SETTING_MAX_FOIL_PRICE: 10,
SETTING_MIN_MISC_PRICE: 0.05,
SETTING_MAX_MISC_PRICE: 10,
SETTING_PRICE_OFFSET: -0.01,
SETTING_PRICE_ALGORITHM: 1,
SETTING_PRICE_IGNORE_LOWEST_Q: 1,
SETTING_LAST_CACHE: 0,
SETTING_RELIST_AUTOMATICALLY: 0,
SETTING_MARKET_PAGE_COUNT: 100
};
function getSettingWithDefault(name) {
return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null);
}
function setSetting(name, value) {
setLocalStorageItem(name, value);
}
//#endregion
//#region Storage
var storagePersistent = localforage.createInstance({
name: 'see_persistent'
});
var storageSession;
var currentUrl = new URL(window.location.href);
var noCache = currentUrl.searchParams.get('no-cache') != null;
// This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache.
// For this reason, a rolling cache is used.
if (getSessionStorageItem('SESSION') == null || noCache) {
var lastCache = getSettingWithDefault(SETTING_LAST_CACHE);
if (lastCache > 5)
lastCache = 0;
setSetting(SETTING_LAST_CACHE, lastCache + 1);
storageSession = localforage.createInstance({
name: 'see_session_' + lastCache
});
storageSession.clear(); // Clear any previous data.
setSessionStorageItem('SESSION', lastCache);
} else {
storageSession = localforage.createInstance({
name: 'see_session_' + getSessionStorageItem('SESSION')
});
}
function getLocalStorageItem(name) {
try {
return localStorage.getItem(name);
} catch (e) {
return null;
}
}
function setLocalStorageItem(name, value) {
try {
localStorage.setItem(name, value);
return true;
} catch (e) {
logConsole('Failed to set local storage item ' + name + ', ' + e + '.')
return false;
}
}
function getSessionStorageItem(name) {
try {
return sessionStorage.getItem(name);
} catch (e) {
return null;
}
}
function setSessionStorageItem(name, value) {
try {
sessionStorage.setItem(name, value);
return true;
} catch (e) {
logConsole('Failed to set session storage item ' + name + ', ' + e + '.')
return false;
}
}
//#endregion
//#region Price helpers
function getPriceInformationFromItem(item) {
var isTradingCard = getIsTradingCard(item);
var isFoilTradingCard = getIsFoilTradingCard(item);
return getPriceInformation(isTradingCard, isFoilTradingCard);
}
function getPriceInformation(isTradingCard, isFoilTradingCard) {
var maxPrice = 0;
var minPrice = 0;
if (!isTradingCard) {
maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE);
minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE);
} else {
maxPrice = isFoilTradingCard
? getSettingWithDefault(SETTING_MAX_FOIL_PRICE)
: getSettingWithDefault(SETTING_MAX_NORMAL_PRICE);
minPrice = isFoilTradingCard
? getSettingWithDefault(SETTING_MIN_FOIL_PRICE)
: getSettingWithDefault(SETTING_MIN_NORMAL_PRICE);
}
maxPrice = maxPrice * 100.0;
minPrice = minPrice * 100.0;
var maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice);
var minPriceBeforeFees = market.getPriceBeforeFees(minPrice);
return { maxPrice, minPrice, maxPriceBeforeFees, minPriceBeforeFees };
}
// Calculates the average history price, before the fee.
function calculateAverageHistoryPriceBeforeFees(history) {
var highest = 0;
var total = 0;
if (history != null) {
// Highest average price in the last 12 hours.
var timeAgo = Date.now() - (12 * 60 * 60 * 1000);
history.forEach(function (historyItem) {
var d = new Date(historyItem[0]);
if (d.getTime() > timeAgo) {
highest += historyItem[1] * historyItem[2];
total += historyItem[2];
}
});
}
if (total == 0)
return 0;
highest = Math.floor(highest / total);
return market.getPriceBeforeFees(highest);
}
// Calculates the listing price, before the fee.
function calculateListingPriceBeforeFees(histogram) {
if (histogram == null ||
histogram.lowest_sell_order == null ||
histogram.sell_order_graph == null)
return 0;
var listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order);
var shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1;
if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) {
var listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100);
if (listingPrice2ndLowest > listingPrice) {
var numberOfListingsLowest = histogram.sell_order_graph[0][1];
var numberOfListings2ndLowest = histogram.sell_order_graph[1][1];
var percentageLower = (100 * (numberOfListingsLowest / numberOfListings2ndLowest));
// The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25).
if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) {
listingPrice = listingPrice2ndLowest;
}
}
}
return listingPrice;
}
// Calculate the sell price based on the history and listings.
// applyOffset specifies whether the price offset should be applied when the listings are used to determine the price.
function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) {
var historyPrice = calculateAverageHistoryPriceBeforeFees(history);
var listingPrice = calculateListingPriceBeforeFees(histogram);
var shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
// If the highest average price is lower than the first listing, return the offset + that listing.
// Otherwise, use the highest average price instead.
var calculatedPrice = 0;
if (historyPrice < listingPrice || !shouldUseAverage) {
calculatedPrice = listingPrice;
} else {
calculatedPrice = historyPrice;
}
var changedToMax = false;
// List for the maximum price if there are no listings yet.
if (calculatedPrice == 0) {
calculatedPrice = maxPriceBeforeFees;
changedToMax = true;
}
// Apply the offset to the calculated price, but only if the price wasn't changed to the max (as otherwise it's impossible to list for this price).
if (!changedToMax && applyOffset) {
calculatedPrice = calculatedPrice + (getSettingWithDefault(SETTING_PRICE_OFFSET) * 100);
}
// Keep our minimum and maximum in mind.
calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees);
// In case there's a buy order higher than the calculated price.
if (histogram != null && histogram.highest_buy_order != null) {
var buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order);
if (buyOrderPrice > calculatedPrice)
calculatedPrice = buyOrderPrice;
}
return calculatedPrice;
}
//#endregion
//#region Integer helpers
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getNumberOfDigits(x) {
return (Math.log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
}
function padLeftZero(str, max) {
str = str.toString();
return str.length < max ? padLeftZero("0" + str, max) : str;
}
function replaceNonNumbers(str) {
return str.replace(/\D/g, '');
}
//#endregion
//#region Steam Market
// Sell an item with a price in cents.
// Price is before fees.
SteamMarket.prototype.sellItem = function (item, price, callback /*err, data*/) {
var sessionId = readCookie('sessionid');
var itemId = item.assetid || item.id;
$.ajax({
type: "POST",
url: 'https://steamcommunity.com/market/sellitem/',
data: {
sessionid: sessionId,
appid: item.appid,
contextid: item.contextid,
assetid: itemId,
amount: 1,
price: price
},
success: function (data) {
callback(ERROR_SUCCESS, data);
},
error: function (data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: { withCredentials: true },
dataType: 'json'
});
};
// Removes an item.
// Item is the unique item id.
SteamMarket.prototype.removeListing = function (item, callback /*err, data*/) {
var sessionId = readCookie('sessionid');
$.ajax({
type: "POST",
url: window.location.protocol + '//steamcommunity.com/market/removelisting/' + item,
data: {
sessionid: sessionId
},
success: function (data) {
callback(ERROR_SUCCESS, data);
},
error: function () {
return callback(ERROR_FAILED);
},
crossDomain: true,
xhrFields: { withCredentials: true },
dataType: 'json'
});
};
// Get the price history for an item.
//
// PriceHistory is an array of prices in the form [data, price, number sold].
// Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]]
// Prices are ordered by oldest to most recent.
// Price is inclusive of fees.
SteamMarket.prototype.getPriceHistory = function (item, cache, callback) {
try {
var market_name = getMarketHashName(item);
if (market_name == null) {
callback(ERROR_FAILED);
return;
}
var appid = item.appid;
if (cache) {
var storage_hash = 'pricehistory_' + appid + '+' + market_name;
storageSession.getItem(storage_hash)
.then(function (value) {
if (value != null)
callback(ERROR_SUCCESS, value, true);
else
market.getCurrentPriceHistory(appid, market_name, callback);
})
.catch(function (error) {
market.getCurrentPriceHistory(appid, market_name, callback);
});
} else
market.getCurrentPriceHistory(appid, market_name, callback);
} catch (e) {
return callback(ERROR_FAILED);
}
};
SteamMarket.prototype.getGooValue = function (item, callback) {
try {
var sessionId = readCookie('sessionid');
$.ajax({
type: "GET",
url: this.inventoryUrlBase + 'ajaxgetgoovalue/',
data: {
sessionid: sessionId,
appid: item.market_fee_app,
assetid: item.assetid,
contextid: item.contextid
},
success: function (data) {
callback(ERROR_SUCCESS, data);
},
error: function (data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: { withCredentials: true },
dataType: 'json'
});
} catch (e) {
return callback(ERROR_FAILED);
}
//http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0
// OR
//http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6
//sessionid=xyz
//appid = 535690
//assetid = 4830605461
//contextid = 6
}
// Grinds the item into gems.
SteamMarket.prototype.grindIntoGoo = function (item, callback) {
try {
var sessionId = readCookie('sessionid');
$.ajax({
type: "POST",
url: this.inventoryUrlBase + 'ajaxgrindintogoo/',
data: {
sessionid: sessionId,
appid: item.market_fee_app,
assetid: item.assetid,
contextid: item.contextid,
goo_value_expected: item.goo_value_expected
},
success: function (data) {
callback(ERROR_SUCCESS, data);
},
error: function (data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: { withCredentials: true },
dataType: 'json'
});
} catch (e) {
return callback(ERROR_FAILED);
}
//sessionid = xyz
//appid = 535690
//assetid = 4830605461
//contextid = 6
//goo_value_expected = 10
//http://steamcommunity.com/my/ajaxgrindintogoo/
}
// Get the current price history for an item.
SteamMarket.prototype.getCurrentPriceHistory = function (appid, market_name, callback) {
var url = window.location.protocol +
'//steamcommunity.com/market/pricehistory/?appid=' +
appid +
'&market_hash_name=' +
market_name;
$.get(url,
function (data) {
if (!data || !data.success || !data.prices) {
callback(ERROR_DATA);
return;
}
// Multiply prices so they're in pennies.
for (var i = 0; i < data.prices.length; i++) {
data.prices[i][1] *= 100;
data.prices[i][2] = parseInt(data.prices[i][2]);
}
// Store the price history in the session storage.
var storage_hash = 'pricehistory_' + appid + '+' + market_name;
storageSession.setItem(storage_hash, data.prices);
callback(ERROR_SUCCESS, data.prices, false);
},
'json')
.fail(function (data) {
if (!data || !data.responseJSON) {
return callback(ERROR_FAILED);
}
if (!data.responseJSON.success) {
callback(ERROR_DATA);
return;
}
return callback(ERROR_FAILED);
});
}
// Get the item name id from a market item.
//
// This id never changes so we can store this in the persistent storage.
SteamMarket.prototype.getMarketItemNameId = function (item, callback) {
try {
var market_name = getMarketHashName(item);
if (market_name == null) {
callback(ERROR_FAILED);
return;
}
var appid = item.appid;
var storage_hash = 'itemnameid_' + appid + '+' + market_name;
storagePersistent.getItem(storage_hash)
.then(function (value) {
if (value != null)
callback(ERROR_SUCCESS, value);
else
return market.getCurrentMarketItemNameId(appid, market_name, callback);
})
.catch(function (error) {
return market.getCurrentMarketItemNameId(appid, market_name, callback);
});
} catch (e) {
return callback(ERROR_FAILED);
}
}
// Get the item name id from a market item.
SteamMarket.prototype.getCurrentMarketItemNameId = function (appid, market_name, callback) {
var url = window.location.protocol + '//steamcommunity.com/market/listings/' + appid + '/' + market_name;
$.get(url,
function (page) {
var matches = /Market_LoadOrderSpread\( (.+) \);/.exec(page);
if (matches == null) {
callback(ERROR_DATA);
return;
}
var item_nameid = matches[1];
// Store the item name id in the persistent storage.
var storage_hash = 'itemnameid_' + appid + '+' + market_name;
storagePersistent.setItem(storage_hash, item_nameid);
callback(ERROR_SUCCESS, item_nameid);
})
.fail(function (e) {
return callback(ERROR_FAILED, e.status);
});
};
// Get the sales listings for this item in the market, with more information.
//
//{
//"success" : 1,
//"sell_order_table" : "<table class=\"market_commodity_orders_table\"><tr><th align=\"right\">Price<\/th><th align=\"right\">Quantity<\/th><\/tr><tr><td align=\"right\" class=\"\">0,04\u20ac<\/td><td align=\"right\">311<\/td><\/tr><tr><td align=\"right\" class=\"\">0,05\u20ac<\/td><td align=\"right\">895<\/td><\/tr><tr><td align=\"right\" class=\"\">0,06\u20ac<\/td><td align=\"right\">495<\/td><\/tr><tr><td align=\"right\" class=\"\">0,07\u20ac<\/td><td align=\"right\">174<\/td><\/tr><tr><td align=\"right\" class=\"\">0,08\u20ac<\/td><td align=\"right\">49<\/td><\/tr><tr><td align=\"right\" class=\"\">0,09\u20ac or more<\/td><td align=\"right\">41<\/td><\/tr><\/table>",
//"sell_order_summary" : "<span class=\"market_commodity_orders_header_promote\">1965<\/span> for sale starting at <span class=\"market_commodity_orders_header_promote\">0,04\u20ac<\/span>",
//"buy_order_table" : "<table class=\"market_commodity_orders_table\"><tr><th align=\"right\">Price<\/th><th align=\"right\">Quantity<\/th><\/tr><tr><td align=\"right\" class=\"\">0,03\u20ac<\/td><td align=\"right\">93<\/td><\/tr><\/table>",
//"buy_order_summary" : "<span class=\"market_commodity_orders_header_promote\">93<\/span> requests to buy at <span class=\"market_commodity_orders_header_promote\">0,03\u20ac<\/span> or lower",
//"highest_buy_order" : "3",
//"lowest_sell_order" : "4",
//"buy_order_graph" : [[0.03, 93, "93 buy orders at 0,03\u20ac or higher"]],
//"sell_order_graph" : [[0.04, 311, "311 sell orders at 0,04\u20ac or lower"], [0.05, 1206, "1,206 sell orders at 0,05\u20ac or lower"], [0.06, 1701, "1,701 sell orders at 0,06\u20ac or lower"], [0.07, 1875, "1,875 sell orders at 0,07\u20ac or lower"], [0.08, 1924, "1,924 sell orders at 0,08\u20ac or lower"], [0.09, 1934, "1,934 sell orders at 0,09\u20ac or lower"], [0.1, 1936, "1,936 sell orders at 0,10\u20ac or lower"], [0.11, 1937, "1,937 sell orders at 0,11\u20ac or lower"], [0.12, 1944, "1,944 sell orders at 0,12\u20ac or lower"], [0.14, 1945, "1,945 sell orders at 0,14\u20ac or lower"]],
//"graph_max_y" : 3000,
//"graph_min_x" : 0.03,
//"graph_max_x" : 0.14,
//"price_prefix" : "",
//"price_suffix" : "\u20ac"
//}
SteamMarket.prototype.getItemOrdersHistogram = function (item, cache, callback) {
try {
var market_name = getMarketHashName(item);
if (market_name == null) {
callback(ERROR_FAILED);
return;
}
var appid = item.appid;
if (cache) {
var storage_hash = 'itemordershistogram_' + appid + '+' + market_name;
storageSession.getItem(storage_hash)
.then(function (value) {
if (value != null)
callback(ERROR_SUCCESS, value, true);
else {
market.getCurrentItemOrdersHistogram(item, market_name, callback);
}
})
.catch(function (error) {
market.getCurrentItemOrdersHistogram(item, market_name, callback);
});
} else {
market.getCurrentItemOrdersHistogram(item, market_name, callback);
}
} catch (e) {
return callback(ERROR_FAILED);
}
};
// Get the sales listings for this item in the market, with more information.
SteamMarket.prototype.getCurrentItemOrdersHistogram = function (item, market_name, callback) {
market.getMarketItemNameId(item,
function (error, item_nameid) {
if (error) {
if (item_nameid != 429) // 429 = Too many requests made.
callback(ERROR_DATA);
else
callback(ERROR_FAILED);
return;
}
var url = window.location.protocol +
'//steamcommunity.com/market/itemordershistogram?language=english&currency=' +
currencyId +
'&item_nameid=' +
item_nameid +
'&two_factor=0';
$.get(url,
function (histogram) {
// Store the histogram in the session storage.
var storage_hash = 'itemordershistogram_' + item.appid + '+' + market_name;
storageSession.setItem(storage_hash, histogram);
callback(ERROR_SUCCESS, histogram, false);
})
.fail(function () {
return callback(ERROR_FAILED, null);
});
});
};
// Calculate the price before fees (seller price) from the buyer price
SteamMarket.prototype.getPriceBeforeFees = function (price, item) {
var publisherFee = -1;
if (item != null) {
if (item.market_fee != null)
publisherFee = item.market_fee;
else if (item.description != null && item.description.market_fee != null)
publisherFee = item.description.market_fee;
}
if (publisherFee == -1) {
if (this.walletInfo != null)
publisherFee = this.walletInfo['wallet_publisher_fee_percent_default'];
else
publisherFee = 0.10;
}
price = Math.round(price);
var feeInfo = CalculateFeeAmount(price, publisherFee, this.walletInfo);
return price - feeInfo.fees;
};
// Calculate the buyer price from the seller price
SteamMarket.prototype.getPriceIncludingFees = function (price, item) {
var publisherFee = -1;
if (item != null) {
if (item.market_fee != null)
publisherFee = item.market_fee;
else if (item.description != null && item.description.market_fee != null)
publisherFee = item.description.market_fee;
}
if (publisherFee == -1) {
if (this.walletInfo != null)
publisherFee = this.walletInfo['wallet_publisher_fee_percent_default'];
else
publisherFee = 0.10;
}
price = Math.round(price);
var feeInfo = CalculateAmountToSendForDesiredReceivedAmount(price, publisherFee, this.walletInfo);
return feeInfo.amount;
};
//#endregion
function replaceAll(str, find, replace) {
return str.replace(new RegExp(find, 'g'), replace);
}
// Cannot use encodeURI / encodeURIComponent, Steam only escapes certain characters.
function escapeURI(name) {
var previousName = '';
while (previousName != name) {
previousName = name;
name = name.replace('?', '%3F')
.replace('#', '%23')
.replace(' ', '%09');
}
return name;
}
//#region Steam Market / Inventory helpers
function getMarketHashName(item) {
if (item == null)
return null;
if (item.description != null && item.description.market_hash_name != null)
return escapeURI(item.description.market_hash_name);
if (item.description != null && item.description.name != null)
return escapeURI(item.description.name);
if (item.market_hash_name != null)
return escapeURI(item.market_hash_name);
if (item.name != null)
return escapeURI(item.name);
return null;
}
function getIsTradingCard(item) {
if (item == null)
return false;
// This is available on the inventory page.
var tags = item.tags != null
? item.tags
: (item.description != null && item.description.tags != null
? item.description.tags
: null);
if (tags != null) {
var isTaggedAsTradingCard = false;
tags.forEach(function (arrayItem) {
if (arrayItem.category == 'item_class')
if (arrayItem.internal_name == 'item_class_2') // trading card.
isTaggedAsTradingCard = true;
});
if (isTaggedAsTradingCard)
return true;
}
// This is available on the market page.
if (item.owner_actions != null) {
for (var i = 0; i < item.owner_actions.length; i++) {
if (item.owner_actions[i].link == null)
continue;
// Cards include a link to the gamecard page.
// For example: "http://steamcommunity.com/my/gamecards/503820/".
if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards'))
return true;
}
}
// A fallback for the market page (only works with language on English).
if (item.type != null && item.type.toLowerCase().includes('trading card'))
return true;
return false;
}
function getIsFoilTradingCard(item) {
if (!getIsTradingCard(item))
return false;
// This is available on the inventory page.
var tags = item.tags != null
? item.tags
: (item.description != null && item.description.tags != null
? item.description.tags
: null);
if (tags != null) {
var isTaggedAsFoilTradingCard = false;
tags.forEach(function (arrayItem) {
if (arrayItem.category == 'cardborder')
if (arrayItem.internal_name == 'cardborder_1') // foil border.
isTaggedAsFoilTradingCard = true;
});
if (isTaggedAsFoilTradingCard)
return true;
}
// This is available on the market page.
if (item.owner_actions != null) {
for (var i = 0; i < item.owner_actions.length; i++) {
if (item.owner_actions[i].link == null)
continue;
// Cards include a link to the gamecard page.
// The border parameter specifies the foil cards.
// For example: "http://steamcommunity.com/my/gamecards/503820/?border=1".
if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards') &&
item.owner_actions[i].link.toString().toLowerCase().includes('border'))
return true;
}
}
// A fallback for the market page (only works with language on English).
if (item.type != null && item.type.toLowerCase().includes('foil trading card'))
return true;
return false;
}
function CalculateFeeAmount(amount, publisherFee, walletInfo) {
if (walletInfo == null || !walletInfo['wallet_fee']) {
return { fees: 0 };
}
publisherFee = (publisherFee == null) ? 0 : publisherFee;
// Since CalculateFeeAmount has a Math.floor, we could be off a cent or two. Let's check:
var iterations = 0; // shouldn't be needed, but included to be sure nothing unforseen causes us to get stuck
var nEstimatedAmountOfWalletFundsReceivedByOtherParty =
parseInt((amount - parseInt(walletInfo['wallet_fee_base'])) /
(parseFloat(walletInfo['wallet_fee_percent']) + parseFloat(publisherFee) + 1));
var bEverUndershot = false;
var fees = CalculateAmountToSendForDesiredReceivedAmount(nEstimatedAmountOfWalletFundsReceivedByOtherParty,
publisherFee,
walletInfo);
while (fees.amount != amount && iterations < 10) {
if (fees.amount > amount) {
if (bEverUndershot) {
fees = CalculateAmountToSendForDesiredReceivedAmount(
nEstimatedAmountOfWalletFundsReceivedByOtherParty - 1,
publisherFee,
walletInfo);
fees.steam_fee += (amount - fees.amount);
fees.fees += (amount - fees.amount);
fees.amount = amount;
break;
} else {
nEstimatedAmountOfWalletFundsReceivedByOtherParty--;
}
} else {
bEverUndershot = true;
nEstimatedAmountOfWalletFundsReceivedByOtherParty++;
}
fees = CalculateAmountToSendForDesiredReceivedAmount(nEstimatedAmountOfWalletFundsReceivedByOtherParty,
publisherFee,
walletInfo);
iterations++;
}
// fees.amount should equal the passed in amount
return fees;
}
// Clamps cur between min and max (inclusive).
function clamp(cur, min, max) {
if (cur < min)
cur = min;
if (cur > max)
cur = max;
return cur;
}
// Strangely named function, it actually works out the fees and buyer price for a seller price
function CalculateAmountToSendForDesiredReceivedAmount(receivedAmount, publisherFee, walletInfo) {
if (walletInfo == null || !walletInfo['wallet_fee']) {
return { amount: receivedAmount };
}
publisherFee = (publisherFee == null) ? 0 : publisherFee;
var nSteamFee = parseInt(Math.floor(Math.max(receivedAmount * parseFloat(walletInfo['wallet_fee_percent']),
walletInfo['wallet_fee_minimum']) +
parseInt(walletInfo['wallet_fee_base'])));
var nPublisherFee = parseInt(Math.floor(publisherFee > 0 ? Math.max(receivedAmount * publisherFee, 1) : 0));
var nAmountToSend = receivedAmount + nSteamFee + nPublisherFee;
return {
steam_fee: nSteamFee,
publisher_fee: nPublisherFee,
fees: nSteamFee + nPublisherFee,
amount: parseInt(nAmountToSend)
};
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ')
c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0)
return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}
//#endregion
//#region Logging
var userScrolled = false;
var logger = document.createElement('div');
logger.setAttribute('id', 'logger');
function updateScroll() {
if (!userScrolled) {
var element = document.getElementById("logger");
element.scrollTop = element.scrollHeight;
}
}
function logDOM(text) {
logger.innerHTML += text + '<br/>';
updateScroll();
}
function clearLogDOM() {
logger.innerHTML = '';
updateScroll();
}
function logConsole(text) {
if (enableConsoleLog) {
console.log(text);
}
}
//#endregion
//#region Inventory
if (currentPage == PAGE_INVENTORY) {
var sellQueue = async.queue(function (task, next) {
market.sellItem(task.item,
task.sellPrice,
function (err, data) {
totalNumberOfProcessedQueueItems++;
var digits = getNumberOfDigits(totalNumberOfQueuedItems);
var itemId = task.item.assetid || task.item.id;
var itemName = task.item.name || task.item.description.name;
var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems;
if (!err) {
logDOM(padLeft +
' - ' +
itemName +
' added to market for ' +
(market.getPriceIncludingFees(task.sellPrice) / 100.0).toFixed(2) +
currencySymbol +
'.');
$('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId)
.css('background', COLOR_SUCCESS);
} else {
if (data.responseJSON.message != null)
logDOM(padLeft +
' - ' +
itemName +
' not added to market because ' +
data.responseJSON.message[0].toLowerCase() +
data.responseJSON.message.slice(1));
else
logDOM(padLeft + ' - ' + itemName + ' not added to market.');
$('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId)
.css('background', COLOR_ERROR);
}
next();
});
},
1);
sellQueue.drain = function () {
if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0) {
$('#inventory_items_spinner').remove();
}
}
function sellAllItems(appId) {
loadAllInventories().then(function () {
var items = getInventoryItems();
var filteredItems = [];
items.forEach(function (item) {
if (!item.marketable) {
return;
}
filteredItems.push(item);
});
sellItems(filteredItems);
},
function () {
logDOM('Could not retrieve the inventory...');
});
}
function sellAllCards() {
loadAllInventories().then(function () {
var items = getInventoryItems();
var filteredItems = [];
items.forEach(function (item) {
if (!getIsTradingCard(item) || !item.marketable) {
return;
}
filteredItems.push(item);
});
sellItems(filteredItems);
},
function () {
logDOM('Could not retrieve the inventory...');
});
}
var scrapQueue = async.queue(function (item, next) {
scrapQueueWorker(item, function (success) {
if (success) {
setTimeout(function () { next(); }, 250);
} else {
var delay = numberOfFailedRequests > 1
? getRandomInt(30000, 45000)
: getRandomInt(1000, 1500);
if (numberOfFailedRequests > 3)
numberOfFailedRequests = 0;
setTimeout(function () {
next();
}, delay);
}
});
}, 1);
scrapQueue.drain = function () {
if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0) {
$('#inventory_items_spinner').remove();
}
}
function scrapQueueWorker(item, callback) {
var failed = 0;
var itemName = item.name || item.description.name;
var itemId = item.assetid || item.id;
market.getGooValue(item,
function (err, goo) {
totalNumberOfProcessedQueueItems++;
var digits = getNumberOfDigits(totalNumberOfQueuedItems);
var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems;
if (err != ERROR_SUCCESS) {
logConsole('Failed to get gems value for ' + itemName);
logDOM(padLeft + ' - ' + itemName + ' not turned into gems due to missing gems value.');
$('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR);
return callback(false);
}
item.goo_value_expected = parseInt(goo.goo_value);
market.grindIntoGoo(item,
function (err, result) {
if (err != ERROR_SUCCESS) {
logConsole('Failed to turn item into gems for ' + itemName);
logDOM(padLeft + ' - ' + itemName + ' not turned into gems due to unknown error.');
$('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR);
return callback(false);
}
logConsole('============================')
logConsole(itemName);
logConsole('Turned into ' + goo.goo_value + ' gems');
logDOM(padLeft + ' - ' + itemName + ' turned into ' + item.goo_value_expected + ' gems.');
$('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_SUCCESS);
callback(true);
});
});
}
// Turns the selected items into gems.
function turnSelectedItemsIntoGems() {
var ids = [];
$('.inventory_ctn').each(function () {
$(this).find('.inventory_page').each(function () {
var inventory_page = this;
$(inventory_page).find('.itemHolder').each(function () {
if (!$(this).hasClass('ui-selected'))
return;
$(this).find('.item').each(function () {
var matches = this.id.match(/_(\-?\d+)$/);
if (matches) {
ids.push(matches[1]);
}
});
});
});
});
loadAllInventories().then(function () {
var items = getInventoryItems();
var numberOfQueuedItems = 0;
items.forEach(function (item) {
// Ignored queued items.
if (item.queued != null) {
return;
}
if (item.owner_actions == null) {
return;
}
var canTurnIntoGems = false;
for (var owner_action in item.owner_actions) {
if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) {
canTurnIntoGems = true;
}
}
if (!canTurnIntoGems)
return;
var itemId = item.assetid || item.id;
if (ids.indexOf(itemId) !== -1) {
item.queued = true;
scrapQueue.push(item);
numberOfQueuedItems++;
}
});
if (numberOfQueuedItems > 0) {
totalNumberOfQueuedItems += numberOfQueuedItems;
$('#inventory_items_spinner').remove();
$('#inventory_sell_buttons').append('<div id="inventory_items_spinner">' +
spinnerBlock +
'<div style="text-align:center">Processing ' + numberOfQueuedItems + ' items</div>' +
'</div>');
}
}, function () {
logDOM('Could not retrieve the inventory...');
});
}
function sellSelectedItems() {
var ids = [];
$('.inventory_ctn').each(function () {
$(this).find('.inventory_page').each(function () {
var inventory_page = this;
$(inventory_page).find('.itemHolder').each(function () {
if (!$(this).hasClass('ui-selected'))
return;
$(this).find('.item').each(function () {
var matches = this.id.match(/_(\-?\d+)$/);
if (matches) {
ids.push(matches[1]);
}
});
});
});
});
loadAllInventories().then(function () {
var items = getInventoryItems();
var filteredItems = [];
items.forEach(function (item) {
if (!item.marketable) {
return;
}
var itemId = item.assetid || item.id;
if (ids.indexOf(itemId) !== -1) {
filteredItems.push(item);
}
});
sellItems(filteredItems);
}, function () {
logDOM('Could not retrieve the inventory...');
});
}
function sellItems(items) {
if (items.length == 0) {
logDOM('These items cannot be added to the market...');
return;
}
var numberOfQueuedItems = 0;
items.forEach(function (item, index, array) {
// Ignored queued items.
if (item.queued != null) {
return;
}
item.queued = true;
var itemId = item.assetid || item.id;
item.ignoreErrors = false;
itemQueue.push(item);
numberOfQueuedItems++;
});
if (numberOfQueuedItems > 0) {
totalNumberOfQueuedItems += numberOfQueuedItems;
$('#inventory_items_spinner').remove();
$('#inventory_sell_buttons').append('<div id="inventory_items_spinner">' +
spinnerBlock +
'<div style="text-align:center">Processing ' + numberOfQueuedItems + ' items</div>' +
'</div>');
}
}
var itemQueue = async.queue(function (item, next) {
itemQueueWorker(item,
item.ignoreErrors,
function (success, cached) {
if (success) {
setTimeout(function () {
next();
},
cached ? 0 : getRandomInt(1000, 1500));
} else {
if (!item.ignoreErrors) {
item.ignoreErrors = true;
itemQueue.push(item);
}
var delay = numberOfFailedRequests > 1
? getRandomInt(30000, 45000)
: getRandomInt(1000, 1500);
if (numberOfFailedRequests > 3)
numberOfFailedRequests = 0;
setTimeout(function () {
next();
},
cached ? 0 : delay);
}
});
}, 1);
function itemQueueWorker(item, ignoreErrors, callback) {
var priceInfo = getPriceInformationFromItem(item);
var failed = 0;
var itemName = item.name || item.description.name;
market.getPriceHistory(item,
true,
function (err, history, cachedHistory) {
if (err) {
logConsole('Failed to get price history for ' + itemName);
if (err == ERROR_FAILED)
failed += 1;
}
market.getItemOrdersHistogram(item,
true,
function (err, histogram, cachedListings) {
if (err) {
logConsole('Failed to get orders histogram for ' + itemName);
if (err == ERROR_FAILED)
failed += 1;
}
if (failed > 0 && !ignoreErrors) {
return callback(false, cachedHistory && cachedListings);
}
logConsole('============================')
logConsole(itemName);
var sellPrice = calculateSellPriceBeforeFees(history,
histogram,
true,
priceInfo.minPriceBeforeFees,
priceInfo.maxPriceBeforeFees);
logConsole('Sell price: ' +
sellPrice / 100.0 +
' (' +
market.getPriceIncludingFees(sellPrice) / 100.0 +
')');
sellQueue.push({
item: item,
sellPrice: sellPrice
});
return callback(true, cachedHistory && cachedListings);
});
});
}
// Initialize the inventory UI.
function initializeInventoryUI() {
var isOwnInventory = g_ActiveUser.strSteamId == g_steamID;
var previousSelection = -1; // To store the index of the previous selection.
updateInventoryUI(isOwnInventory);
$('.games_list_tabs').on('click',
'*',
function () {
updateInventoryUI(isOwnInventory);
});
// Ignore selection on other user's inventories.
if (!isOwnInventory)
return;
var filter =
".itemHolder:not([style*=none])"; // Steam adds 'display:none' to items while searching. These should not be selected while using shift/ctrl.
$('#inventories').selectable({
filter: filter,
selecting: function (e, ui) {
var selectedIndex =
$(ui.selecting.tagName, e.target).index(ui.selecting); // Get selected item index.
if (e.shiftKey && previousSelection > -1
) { // If shift key was pressed and there is previous - select them all.
$(ui.selecting.tagName, e.target)
.slice(Math.min(previousSelection, selectedIndex),
1 + Math.max(previousSelection, selectedIndex)).each(function () {
if ($(this).is(filter)) {
$(this).addClass('ui-selected');
}
});
previousSelection = -1; // Reset previous.
} else {
previousSelection = selectedIndex; // Save previous.
}
},
selected: function (e, ui) {
updateInventorySelection(ui.selected);
}
});
}
function updateInventorySelection(item) {
// Wait until g_ActiveInventory.selectedItem is identical to the selected UI item.
// This also makes sure that the new - and correct - item_info (iteminfo0 or iteminfo1) is visible.
var selectedItemIdUI = $('div', item).attr('id');
var selectedItemIdInventory = getActiveInventory().selectedItem.appid +
'_' +
getActiveInventory().selectedItem.contextid +
'_' +
getActiveInventory().selectedItem.assetid;
if (selectedItemIdUI !== selectedItemIdInventory) {
setTimeout(function () {
updateInventorySelection(item);
},
250);
return;
}
var item_info = $('.inventory_iteminfo:visible').first();
if (item_info.html().indexOf('checkout/sendgift/') > -1) // Gifts have no market information.
return;
// Use a 'hard' item id instead of relying on the selected item_info (sometimes Steam temporarily changes the correct item (?)).
var item_info_id = item_info.attr('id');
// Move scrap to bottom, this is of little interest.
var scrap = $('#' + item_info_id + '_scrap_content');
scrap.next().insertBefore(scrap);
// Starting at prices are already retrieved in the table.
//$('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(2)')
// .remove(); // Starting at: x,xx.
var market_hash_name = getMarketHashName(getActiveInventory().selectedItem);
if (market_hash_name == null)
return;
var appid = getActiveInventory().selectedItem.appid;
var item = { appid: parseInt(appid), description: { market_hash_name: market_hash_name } };
market.getItemOrdersHistogram(item,
false,
function (err, histogram) {
if (err) {
logConsole('Failed to get orders histogram for ' + (getActiveInventory().selectedItem.name || getActiveInventory().selectedItem.description.name));
return;
}
var groupMain = $('<div id="listings_group">' +
'<div><div id="listings_sell">Продажа</div>' +
histogram.sell_order_table +
'</div>' +
'<div><div id="listings_buy">Покупка</div>' +
histogram.buy_order_table +
'</div>' +
'</div>');
$('#' + item_info_id + '_item_market_actions > div').after(groupMain);
// Generate quick sell buttons.
var itemId = getActiveInventory().selectedItem.assetid || getActiveInventory().selectedItem.id;
// Ignored queued items.
if (getActiveInventory().selectedItem.queued != null) {
return;
}
var prices = [];
if (histogram != null && histogram.highest_buy_order != null) {
prices.push(parseInt(histogram.highest_buy_order));
}
if (histogram != null && histogram.lowest_sell_order != null) {
prices.push(parseInt(histogram.lowest_sell_order) - 1);
prices.push(parseInt(histogram.lowest_sell_order));
prices.push(parseInt(histogram.lowest_sell_order) + 1);
}
var priceInformation = getPriceInformationFromItem(getActiveInventory().selectedItem);
prices.push(priceInformation.minPrice);
prices.push(priceInformation.maxPrice);
prices = prices.filter((v, i) => prices.indexOf(v) === i).sort((a, b) => a - b);
var buttons = '<br/>';
prices.forEach(function (e) {
buttons +=
'<a class="item_market_action_button item_market_action_button_green quick_sell" id="quick_sell' +
e +
'">' +
'<span class="item_market_action_button_edge item_market_action_button_left"></span>' +
'<span class="item_market_action_button_contents">' +
(e / 100.0) +
currencySymbol +
'</span>' +
'<span class="item_market_action_button_edge item_market_action_button_right"></span>' +
'<span class="item_market_action_button_preload"></span>' +
'</a>'
});
$('#' + item_info_id + '_item_market_actions', item_info).append(buttons);
$('.quick_sell').on('click',
function () {
var price = $(this).attr('id').replace('quick_sell', '');
price = market.getPriceBeforeFees(price);
totalNumberOfQueuedItems++;
sellQueue.push({
item: getActiveInventory().selectedItem,
sellPrice: price
});
});
});
}
// Update the inventory UI.
function updateInventoryUI(isOwnInventory) {
// Remove previous containers (e.g., when a user changes inventory).
$('#inventory_sell_buttons').remove();
$('#price_options').remove();
$('#inventory_reload_button').remove();
$('#see_settings').remove();
$('#global_action_menu')
.prepend('<span id="see_settings"><a href="javascript:void(0)">⬖ Steam Economy Enhancer</a></span>');
$('#see_settings').on('click', '*', () => openSettings());
var appId = getActiveInventory().m_appid;
var showMiscOptions = appId == 753;
var sellButtons = $('<div id="inventory_sell_buttons" style="margin-bottom:12px;">' +
'<a class="btn_green_white_innerfade btn_medium_wide sell_all"><span>Продать все предметы</span></a>&nbsp;&nbsp;&nbsp;' +
'<a class="btn_green_white_innerfade btn_medium_wide sell_selected"><span>Продать выбранные предметы</span></a>&nbsp;&nbsp;&nbsp;' +
(showMiscOptions
? '<a class="btn_green_white_innerfade btn_medium_wide turn_into_gems"><span>Скрафтить выбранное в самоцветы</span></a>&nbsp;&nbsp;&nbsp;' +
'<a class="btn_darkblue_white_innerfade btn_medium_wide sell_all_cards"><span>Продать все карты</span></a>&nbsp;&nbsp;&nbsp;'
: '') +
'</div>');
var reloadButton =
$(
'<a id="inventory_reload_button" class="btn_darkblue_white_innerfade btn_medium_wide reload_inventory" style="margin-right:12px"><span>Перезагрузить инвентарь</span></a>');
$('#inventory_logos')[0].style.height = 'auto';
$('#inventory_applogo').hide(); // Hide the Steam/game logo, we don't need to see it twice.
$('#inventory_applogo').after(logger);
$("#logger").on('scroll',
function () {
var hasUserScrolledToBottom =
$("#logger").prop('scrollHeight') - $("#logger").prop('clientHeight') <=
$("#logger").prop('scrollTop') + 1;
userScrolled = !hasUserScrolledToBottom;
});
// Only add buttons on the user's inventory.
if (isOwnInventory) {
$('#inventory_applogo').after(sellButtons);
// Add bindings to sell buttons.
$('.sell_all').on('click',
'*',
function () {
sellAllItems(appId);
});
$('.sell_selected').on('click', '*', sellSelectedItems);
$('.sell_all_cards').on('click', '*', sellAllCards);
$('.turn_into_gems').on('click', '*', turnSelectedItemsIntoGems);
}
$('.inventory_rightnav').prepend(reloadButton);
$('.reload_inventory').on('click',
'*',
function () {
window.location.reload();
});
loadAllInventories().then(function () {
var updateInventoryPrices = function () {
setInventoryPrices(getInventoryItems());
};
// Load after the inventory is loaded.
updateInventoryPrices();
$('#inventory_pagecontrols').observe('childlist',
'*',
function (record) {
updateInventoryPrices();
});
},
function () {
logDOM('Could not retrieve the inventory...');
});
}
// Loads the specified inventories.
function loadInventories(inventories) {
return new Promise(function (resolve) {
inventories.reduce(function (promise, inventory) {
return promise.then(function () {
return inventory.LoadCompleteInventory().done(function () { });
});
},
Promise.resolve());
resolve();
});
}
// Loads all inventories.
function loadAllInventories() {
var items = [];
for (var child in getActiveInventory().m_rgChildInventories) {
items.push(getActiveInventory().m_rgChildInventories[child]);
}
items.push(getActiveInventory());
return loadInventories(items);
}
// Gets the inventory items from the active inventory.
function getInventoryItems() {
var arr = [];
for (var child in getActiveInventory().m_rgChildInventories) {
for (var key in getActiveInventory().m_rgChildInventories[child].m_rgAssets) {
var value = getActiveInventory().m_rgChildInventories[child].m_rgAssets[key];
if (typeof value === 'object') {
// Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened.
Object.assign(value, value.description);
// Includes the id of the inventory item.
value['id'] = key;
arr.push(value);
}
}
}
// Some inventories (e.g. BattleBlock Theater) do not have child inventories, they have just one.
for (var key in getActiveInventory().m_rgAssets) {
var value = getActiveInventory().m_rgAssets[key];
if (typeof value === 'object') {
// Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened.
Object.assign(value, value.description);
// Includes the id of the inventory item.
value['id'] = key;
arr.push(value);
}
}
return arr;
}
}
//#endregion
//#region Inventory + Tradeoffer
if (currentPage == PAGE_INVENTORY || currentPage == PAGE_TRADEOFFER) {
// Gets the active inventory.
function getActiveInventory() {
return g_ActiveInventory;
}
// Sets the prices for the items.
function setInventoryPrices(items) {
inventoryPriceQueue.kill();
items.forEach(function (item) {
if (!item.marketable) {
return;
}
if (!$(item.element).is(":visible"))
return;
inventoryPriceQueue.push(item);
});
}
var inventoryPriceQueue = async.queue(function (item, next) {
inventoryPriceQueueWorker(item,
false,
function (success, cached) {
if (success) {
setTimeout(function () {
next();
},
cached ? 0 : getRandomInt(1000, 1500));
} else {
if (!item.ignoreErrors) {
item.ignoreErrors = true;
inventoryPriceQueue.push(item);
}
numberOfFailedRequests++;
var delay = numberOfFailedRequests > 1
? getRandomInt(30000, 45000)
: getRandomInt(1000, 1500);
if (numberOfFailedRequests > 3)
numberOfFailedRequests = 0;
setTimeout(function () {
next();
},
cached ? 0 : delay);
}
});
},
1);
function inventoryPriceQueueWorker(item, ignoreErrors, callback) {
var priceInfo = getPriceInformationFromItem(item);
var failed = 0;
var itemName = item.name || item.description.name;
// Only get the market orders here, the history is not important to visualize the current prices.
market.getItemOrdersHistogram(item,
true,
function (err, histogram, cachedListings) {
if (err) {
logConsole('Failed to get orders histogram for ' + itemName);
if (err == ERROR_FAILED)
failed += 1;
}
if (failed > 0 && !ignoreErrors) {
return callback(false, cachedListings);
}
var sellPrice = calculateSellPriceBeforeFees(null, histogram, false, 0, 65535);
var itemPrice = sellPrice == 65535
? '∞'
: (market.getPriceIncludingFees(sellPrice) / 100.0).toFixed(2) + currencySymbol;
var elementName = (currentPage == PAGE_TRADEOFFER ? '#item' : '#') +
item.appid +
'_' +
item.contextid +
'_' +
item.id;
var element = $(elementName);
$('.inventory_item_price', element).remove();
element.append('<span class="inventory_item_price price_' + (sellPrice == 65535 ? 0 : market.getPriceIncludingFees(sellPrice)) + '">' + itemPrice + '</span>');
return callback(true, cachedListings);
});
}
}
//#endregion
//#region Market
if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) {
var marketListingsQueue = async.queue(function (listing, next) {
marketListingsQueueWorker(listing,
false,
function (success, cached) {
if (success) {
setTimeout(function () {
next();
},
cached ? 0 : getRandomInt(1000, 1500));
} else {
setTimeout(function () {
marketListingsQueueWorker(listing,
true,
function (success, cached) {
next(); // Go to the next queue item, regardless of success.
});
},
cached ? 0 : getRandomInt(30000, 45000));
}
});
},
1);
marketListingsQueue.drain = function () {
injectJs(function () {
g_bMarketWindowHidden = false;
})
};
function marketListingsQueueWorker(listing, ignoreErrors, callback) {
var asset = g_rgAssets[listing.appid][listing.contextid][listing.assetid];
// An asset:
//{
// "currency" : 0,
// "appid" : 753,
// "contextid" : "6",
// "id" : "4363079664",
// "classid" : "2228526061",
// "instanceid" : "0",
// "amount" : "1",
// "status" : 2,
// "original_amount" : "1",
// "background_color" : "",
// "icon_url" : "xx",
// "icon_url_large" : "xxx",
// "descriptions" : [{
// "value" : "Their dense, shaggy fur conceals the presence of swams of moogamites, purple scaly skin, and more nipples than one would expect."
// }
// ],
// "tradable" : 1,
// "owner_actions" : [{
// "link" : "http://steamcommunity.com/my/gamecards/443880/",
// "name" : "View badge progress"
// }, {
// "link" : "javascript:GetGooValue( '%contextid%', '%assetid%', 443880, 7, 0 )",
// "name" : "Turn into Gems..."
// }
// ],
// "name" : "Wook",
// "type" : "Loot Rascals Trading Card",
// "market_name" : "Wook",
// "market_hash_name" : "443880-Wook",
// "market_fee_app" : 443880,
// "commodity" : 1,
// "market_tradable_restriction" : 7,
// "market_marketable_restriction" : 7,
// "marketable" : 1,
// "app_icon" : "xxxx",
// "owner" : 0
//}
var market_hash_name = getMarketHashName(asset);
var appid = listing.appid;
var listingUI = $(getListingFromLists(listing.listingid).elm);
var game_name = asset.type;
var priceLabel = $('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listingUI).text().trim().replace('--', '00');
// Fixes RUB, which has a dot at the end.
if (priceLabel[priceLabel.length - 1] === '.' || priceLabel[priceLabel.length - 1] === ",")
priceLabel = priceLabel.slice(0, -1);
// For round numbers (e.g., 100 EUR).
if (priceLabel.indexOf('.') === -1 && priceLabel.indexOf(',') === -1) {
priceLabel = priceLabel + ',00';
}
var price = parseInt(replaceNonNumbers(priceLabel));
var priceInfo = getPriceInformationFromItem(asset);
var item = { appid: parseInt(appid), description: { market_hash_name: market_hash_name } };
var failed = 0;
market.getPriceHistory(item,
true,
function (errorPriceHistory, history, cachedHistory) {
if (errorPriceHistory) {
logConsole('Failed to get price history for ' + game_name);
if (errorPriceHistory == ERROR_FAILED)
failed += 1;
}
market.getItemOrdersHistogram(item,
true,
function (errorHistogram, histogram, cachedListings) {
if (errorHistogram) {
logConsole('Failed to get orders histogram for ' + game_name);
if (errorHistogram == ERROR_FAILED)
failed += 1;
}
if (failed > 0 && !ignoreErrors) {
return callback(false, cachedHistory && cachedListings);
}
// Shows the highest buy order price on the market listings.
// The 'histogram.highest_buy_order' is not reliable as Steam is caching this value, but it gives some idea for older titles/listings.
var highestBuyOrderPrice = (histogram == null || histogram.highest_buy_order == null
? '-'
: ((histogram.highest_buy_order / 100) + currencySymbol));
$('.market_table_value > span:nth-child(1) > span:nth-child(1) > span:nth-child(1)',
listingUI).append(' ➤ <span title="This is likely the highest buy order price.">' +
highestBuyOrderPrice +
'</span>');
logConsole('============================')
logConsole(JSON.stringify(listing));
logConsole(game_name + ': ' + asset.name);
logConsole('Current price: ' + price / 100.0);
// Calculate two prices here, one without the offset and one with the offset.
// The price without the offset is required to not relist the item constantly when you have the lowest price (i.e., with a negative offset).
// The price with the offset should be used for relisting so it will still apply the user-set offset.
var sellPriceWithoutOffset = calculateSellPriceBeforeFees(history,
histogram,
false,
priceInfo.minPriceBeforeFees,
priceInfo.maxPriceBeforeFees);
var sellPriceWithOffset = calculateSellPriceBeforeFees(history,
histogram,
true,
priceInfo.minPriceBeforeFees,
priceInfo.maxPriceBeforeFees);
var sellPriceWithoutOffsetWithFees = market.getPriceIncludingFees(sellPriceWithoutOffset);
logConsole('Calculated price: ' +
sellPriceWithoutOffsetWithFees / 100.0 +
' (' +
sellPriceWithoutOffset / 100.0 +
')');
listingUI.addClass('price_' + sellPriceWithOffset);
$('.market_listing_my_price', listingUI).last().prop('title',
'The best price is ' + (sellPriceWithoutOffsetWithFees / 100.0) + currencySymbol + '.');
if (sellPriceWithoutOffsetWithFees < price) {
logConsole('Sell price is too high.');
$('.market_listing_my_price', listingUI).last()
.css('background', COLOR_PRICE_EXPENSIVE);
listingUI.addClass('overpriced');
if (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1) {
queueOverpricedItemListing(listing.listingid);
}
} else if (sellPriceWithoutOffsetWithFees > price) {
logConsole('Sell price is too low.');
$('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_CHEAP);
listingUI.addClass('underpriced');
} else {
logConsole('Sell price is fair.');
$('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_FAIR);
listingUI.addClass('fair');
}
return callback(true, cachedHistory && cachedListings);
});
});
}
var marketOverpricedQueue = async.queue(function (item, next) {
marketOverpricedQueueWorker(item,
false,
function (success) {
if (success) {
setTimeout(function () {
next();
},
getRandomInt(1000, 1500));
} else {
setTimeout(function () {
marketOverpricedQueueWorker(item,
true,
function (success) {
next(); // Go to the next queue item, regardless of success.
});
},
getRandomInt(30000, 45000));
}
});
},
1);
function marketOverpricedQueueWorker(item, ignoreErrors, callback) {
var listingUI = getListingFromLists(item.listing).elm;
market.removeListing(item.listing,
function (errorRemove, data) {
if (!errorRemove) {
$('.actual_content', listingUI).css('background', COLOR_PENDING);
setTimeout(function () {
var baseUrl = $('.header_notification_items').first().attr('href') + 'json/';
var itemName = $('.market_listing_item_name_link', listingUI).first().attr('href');
var marketHashNameIndex = itemName.lastIndexOf('/') + 1;
var marketHashName = itemName.substring(marketHashNameIndex);
var decodedMarketHashName = decodeURIComponent(itemName.substring(marketHashNameIndex));
var newAssetId = -1;
RequestFullInventory(baseUrl + item.appid + "/" + item.contextid + "/", {}, null, null, function (transport) {
if (transport.responseJSON && transport.responseJSON.success) {
var inventory = transport.responseJSON.rgInventory;
for (var child in inventory) {
if (inventory[child].appid == item.appid &&
(inventory[child].market_hash_name == decodedMarketHashName ||
inventory[child].market_hash_name == marketHashName)) {
newAssetId = child;
}
}
if (newAssetId == -1) {
$('.actual_content', listingUI).css('background', COLOR_ERROR);
return callback(false);
}
item.assetid = newAssetId;
market.sellItem(item,
item.sellPrice,
function (errorSell) {
if (!errorSell) {
$('.actual_content', listingUI).css('background', COLOR_SUCCESS);
setTimeout(function () { removeListingFromLists(item.listing) }, 3000);
return callback(true);
} else {
$('.actual_content', listingUI).css('background', COLOR_ERROR);
return callback(false);
}
});
} else {
$('.actual_content', listingUI).css('background', COLOR_ERROR);
return callback(false);
}
});
}, getRandomInt(1500, 2500)); // Wait a little to make sure the item is returned to inventory.
} else {
$('.actual_content', listingUI).css('background', COLOR_ERROR);
return callback(false);
}
});
}
// Queue an overpriced item listing to be relisted.
function queueOverpricedItemListing(listingid) {
var assetInfo = getAssetInfoFromListingId(listingid);
var listingUI = $(getListingFromLists(listingid).elm);
var price = -1;
var items = $(listingUI).attr('class').split(' ');
for (var i in items) {
if (items[i].toString().includes('price_'))
price = parseInt(items[i].toString().replace('price_', ''));
}
if (price > 0) {
marketOverpricedQueue.push({
listing: listingid,
assetid: assetInfo.assetid,
contextid: assetInfo.contextid,
appid: assetInfo.appid,
sellPrice: price
});
}
}
var marketRemoveQueue = async.queue(function (listingid, next) {
marketRemoveQueueWorker(listingid,
false,
function (success) {
if (success) {
setTimeout(function () {
next();
},
getRandomInt(50, 100));
} else {
setTimeout(function () {
marketRemoveQueueWorker(listingid,
true,
function (success) {
next(); // Go to the next queue item, regardless of success.
});
},
getRandomInt(30000, 45000));
}
});
},
10);
function marketRemoveQueueWorker(listingid, ignoreErrors, callback) {
var listingUI = getListingFromLists(listingid).elm;
market.removeListing(listingid,
function (errorRemove, data) {
if (!errorRemove) {
$('.actual_content', listingUI).css('background', COLOR_SUCCESS);
setTimeout(function () {
removeListingFromLists(listingid);
var numberOfListings = marketLists[0].size;
if (numberOfListings > 0) {
$('#my_market_selllistings_number').text((numberOfListings).toString());
// This seems identical to the number of sell listings.
$('#my_market_activelistings_number').text((numberOfListings).toString());
}
},
3000);
return callback(true);
} else {
$('.actual_content', listingUI).css('background', COLOR_ERROR);
return callback(false);
}
});
}
var marketListingsItemsQueue = async.queue(function (listing, next) {
$.get(window.location.protocol + '//steamcommunity.com/market/mylistings?count=100&start=' + listing,
function (data) {
if (!data || !data.success) {
next();
return;
}
var myMarketListings = $('#tabContentsMyActiveMarketListingsRows');
var nodes = $.parseHTML(data.results_html);
var rows = $('.market_listing_row', nodes);
myMarketListings.append(rows);
// g_rgAssets
MergeWithAssetArray(data.assets); // This is a method from Steam.
next();
},
'json')
.fail(function (data) {
next();
return;
});
},
1);
marketListingsItemsQueue.drain = function () {
var myMarketListings = $('#tabContentsMyActiveMarketListingsRows');
myMarketListings.checkboxes('range', true);
// Sometimes the Steam API is returning duplicate entries (especially during item listing), filter these.
var seen = {};
$('.market_listing_row', myMarketListings).each(function () {
var item_id = $(this).attr('id');
if (seen[item_id])
$(this).remove();
else
seen[item_id] = true;
// Remove listings awaiting confirmations, they are already listed separately.
if ($('.item_market_action_button', this).attr('href').toLowerCase()
.includes('CancelMarketListingConfirmation'.toLowerCase()))
$(this).remove();
// Remove buy order listings, they are already listed separately.
if ($('.item_market_action_button', this).attr('href').toLowerCase()
.includes('CancelMarketBuyOrder'.toLowerCase()))
$(this).remove();
});
// Now add the market checkboxes.
addMarketCheckboxes();
// Show the listings again, rendering is done.
$('#market_listings_spinner').remove();
myMarketListings.show();
fillMarketListingsQueue();
injectJs(function () {
g_bMarketWindowHidden =
true; // Limits the number of requests made to steam by stopping constant polling of popular listings.
});
};
function fillMarketListingsQueue() {
$('.market_home_listing_table').each(function (e) {
// Not for popular / new / recently sold items (bottom of page).
if ($('.my_market_header', $(this)).length == 0)
return;
// Buy orders and listings confirmations are not grouped like the sell listings, add this so pagination works there as well.
if (!$(this).attr('id')) {
$(this).attr('id', 'market-listing-' + e);
$(this).append('<div class="market_listing_see" id="market-listing-container-' + e + '"></div>')
$('.market_listing_row', $(this)).appendTo($('#market-listing-container-' + e));
} else {
$(this).children().last().addClass("market_listing_see");
}
addMarketPagination($('.market_listing_see', this).last());
sortMarketListings($(this), false, false, true);
});
// Add the listings to the queue to be checked for the price.
for (var i = 0; i < marketLists.length; i++) {
for (var j = 0; j < marketLists[i].items.length; j++) {
var listingid = replaceNonNumbers(marketLists[i].items[j].values().market_listing_item_name);
var assetInfo = getAssetInfoFromListingId(listingid);
marketListingsQueue.push({
listingid,
appid: assetInfo.appid,
contextid: assetInfo.contextid,
assetid: assetInfo.assetid
});
}
}
}
// Gets the asset info (appid/contextid/assetid) based on a listingid.
function getAssetInfoFromListingId(listingid) {
var listing = getListingFromLists(listingid);
if (listing == null) {
return {};
}
var actionButton = $('.item_market_action_button', listing.elm).attr('href');
// Market buy orders have no asset info.
if (actionButton == null || actionButton.toLowerCase().includes('cancelmarketbuyorder'))
return {};
var itemIds = actionButton.split(',');
var appid = replaceNonNumbers(itemIds[2]);
var contextid = replaceNonNumbers(itemIds[3]);
var assetid = replaceNonNumbers(itemIds[4]);
return { appid, contextid, assetid };
}
// Adds pagination and search options to the market item listings.
function addMarketPagination(market_listing_see) {
market_listing_see.addClass('list');
market_listing_see.before('<ul class="paginationTop pagination"></ul>');
market_listing_see.after('<ul class="paginationBottom pagination"></ul>');
$('.market_listing_table_header', market_listing_see.parent())
.append('<input class="search" id="market_name_search" placeholder="Search..." />');
var options = {
valueNames: [
'market_listing_game_name', 'market_listing_item_name_link', 'market_listing_price',
'market_listing_listed_date', { name: 'market_listing_item_name', attr: 'id' }
],
pagination: [
{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 100,
outerWindow: 100,
left: 100,
right: 100
}, {
name: "paginationBottom",
paginationClass: "paginationBottom",
innerWindow: 100,
outerWindow: 100,
left: 100,
right: 100
}
],
page: parseInt(getSettingWithDefault(SETTING_MARKET_PAGE_COUNT))
};
var list = new List(market_listing_see.parent().attr('id'), options);
marketLists.push(list);
}
// Adds checkboxes to market listings.
function addMarketCheckboxes() {
$('.market_listing_row').each(function () {
// Don't add it again, one time is enough.
if ($('.market_listing_select', this).length == 0) {
$('.market_listing_cancel_button', $(this)).append('<div class="market_listing_select">' +
'<input type="checkbox" class="market_select_item"/>' +
'</div>');
$('.market_select_item', this).change(function (e) {
updateMarketSelectAllButton();
});
}
});
}
// Process the market listings.
function processMarketListings() {
addMarketCheckboxes();
if (currentPage == PAGE_MARKET) {
// Load the market listings.
var currentCount = 0;
var totalCount = 0;
if (typeof g_oMyListings !== 'undefined' && g_oMyListings != null && g_oMyListings.m_cTotalCount != null)
totalCount = g_oMyListings.m_cTotalCount;
else {
totalCount = parseInt($('#my_market_selllistings_number').text());
}
if (isNaN(totalCount) || totalCount == 0) {
fillMarketListingsQueue();
return;
}
$('#tabContentsMyActiveMarketListingsRows').html(''); // Clear the default listings.
$('#tabContentsMyActiveMarketListingsRows').hide(); // Hide all listings until everything has been loaded.
// Hide Steam's paging controls.
$('#tabContentsMyActiveMarketListings_ctn').hide();
$('.market_pagesize_options').hide();
// Show the spinner so the user knows that something is going on.
$('.my_market_header').eq(0).append('<div id="market_listings_spinner">' +
spinnerBlock +
'<div style="text-align:center">Loading market listings</div>' +
'</div>');
while (currentCount < totalCount) {
marketListingsItemsQueue.push(currentCount);
currentCount += 100;
}
} else {
// This is on a market item page.
$('.market_home_listing_table').each(function (e) {
// Not on 'x requests to buy at y,yy or lower'.
if ($('#market_buyorder_info_show_details', $(this)).length > 0)
return;
$(this).children().last().addClass("market_listing_see");
addMarketPagination($('.market_listing_see', this).last());
sortMarketListings($(this), false, false, true);
});
$('#tabContentsMyActiveMarketListingsRows > .market_listing_row').each(function () {
var listingid = $(this).attr('id').replace('mylisting_', '').replace('mybuyorder_', '').replace('mbuyorder_', '');
var assetInfo = getAssetInfoFromListingId(listingid);
// There's only one item in the g_rgAssets on a market listing page.
var existingAsset = null;
for (var appid in g_rgAssets) {
for (var contextid in g_rgAssets[appid]) {
for (var assetid in g_rgAssets[appid][contextid]) {
existingAsset = g_rgAssets[appid][contextid][assetid];
break;
}
}
}
// appid and contextid are identical, only the assetid is different for each asset.
g_rgAssets[appid][contextid][assetInfo.assetid] = existingAsset;
marketListingsQueue.push({ listingid, appid: assetInfo.appid, contextid: assetInfo.contextid, assetid: assetInfo.assetid });
})
}
}
// Update the select/deselect all button on the market.
function updateMarketSelectAllButton() {
$('.market_listing_buttons').each(function () {
var selectionGroup = $(this).parent().parent();
var invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length;
if ($('.market_select_item', selectionGroup).length == 0) // If there are no items to select, keep it at Select all.
invert = false;
$('.select_all > span', selectionGroup).text(invert ? 'Deselect all' : 'Select all');
});
}
// Sort the market listings.
function sortMarketListings(elem, isPrice, isDate, isName) {
var list = getListFromContainer(elem);
if (list == null) {
console.log('Invalid parameter, could not find a list matching elem.');
return;
}
// Change sort order (asc/desc).
var nextSort = isPrice ? 1 : (isDate ? 2 : 3);
var asc = true;
// (Re)set the asc/desc arrows.
const arrow_down = '🡻';
const arrow_up = '🡹';
$('.market_listing_table_header > span', elem).each(function () {
if ($(this).hasClass('market_listing_edit_buttons'))
return;
if ($(this).text().includes(arrow_up))
asc = false;
$(this).text($(this).text().replace(' ' + arrow_down, '').replace(' ' + arrow_up, ''));
})
var market_listing_selector;
if (isPrice) {
market_listing_selector = $('.market_listing_table_header', elem).children().eq(1);
} else if (isDate) {
market_listing_selector = $('.market_listing_table_header', elem).children().eq(2);
} else if (isName) {
market_listing_selector = $('.market_listing_table_header', elem).children().eq(3);
}
market_listing_selector.text(market_listing_selector.text() + ' ' + (asc ? arrow_up : arrow_down));
if (list.sort == null)
return;
if (isName) {
list.sort('',
{
order: asc ? "asc" : "desc",
sortFunction:
function (a, b) {
if (a.values().market_listing_game_name.toLowerCase()
.localeCompare(b.values().market_listing_game_name.toLowerCase()) ==
0) {
return a.values().market_listing_item_name_link.toLowerCase()
.localeCompare(b.values().market_listing_item_name_link.toLowerCase());
}
return a.values().market_listing_game_name.toLowerCase()
.localeCompare(b.values().market_listing_game_name.toLowerCase());
}
});
} else if (isDate) {
var currentMonth = parseInt(Date.today().toString('M'));
list.sort('market_listing_listed_date', {
order: asc ? "asc" : "desc", sortFunction: function (a, b) {
var firstDate = Date.parse((a.values().market_listing_listed_date).trim());
var secondDate = Date.parse((b.values().market_listing_listed_date).trim());
if (firstDate == null || secondDate == null) {
return 0;
}
if (parseInt(firstDate.toString('M')) > currentMonth)
firstDate = firstDate.addYears(-1);
if (parseInt(secondDate.toString('M')) > currentMonth)
secondDate = secondDate.addYears(-1);
return firstDate.compareTo(secondDate);
}
})
} else if (isPrice) {
list.sort('market_listing_price', {
order: asc ? "asc" : "desc", sortFunction: function (a, b) {
var listingPriceA = $(a.values().market_listing_price).text();
listingPriceA = listingPriceA.substr(0, listingPriceA.indexOf('('));
listingPriceA = listingPriceA.replace('--', '00');
var listingPriceB = $(b.values().market_listing_price).text();
listingPriceB = listingPriceB.substr(0, listingPriceB.indexOf('('));
listingPriceB = listingPriceB.replace('--', '00');
var firstPrice = parseInt(replaceNonNumbers(listingPriceA));
var secondPrice = parseInt(replaceNonNumbers(listingPriceB));
return firstPrice - secondPrice;
}
})
}
}
function getListFromContainer(group) {
for (var i = 0; i < marketLists.length; i++) {
if (group.attr('id') == $(marketLists[i].listContainer).attr('id'))
return marketLists[i];
}
}
function getListingFromLists(listingid) {
// Sometimes listing ids are contained in multiple lists (?), use the last one available as this is the one we're most likely interested in.
for (var i = marketLists.length - 1; i >= 0; i--) {
var values = marketLists[i].get("market_listing_item_name", 'mylisting_' + listingid + '_name');
if (values != null && values.length > 0) {
return values[0];
}
values = marketLists[i].get("market_listing_item_name", 'mbuyorder_' + listingid + '_name');
if (values != null && values.length > 0) {
return values[0];
}
}
}
function removeListingFromLists(listingid) {
for (var i = 0; i < marketLists.length; i++) {
marketLists[i].remove("market_listing_item_name", 'mylisting_' + listingid + '_name');
marketLists[i].remove("market_listing_item_name", 'mbuyorder_' + listingid + '_name');
}
}
// Initialize the market UI.
function initializeMarketUI() {
// Sell orders.
$('.my_market_header').first().append(
'<div class="market_listing_buttons">' +
'<a class="item_market_action_button item_market_action_button_green select_all market_listing_button">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Select all</span>' +
'</a>' +
'<span class="separator-small"></span>' +
'<a class="item_market_action_button item_market_action_button_green remove_selected market_listing_button">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Remove selected</span>' +
'</a>' +
'<a class="item_market_action_button item_market_action_button_green relist_selected market_listing_button market_listing_button_right">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Relist selected</span>' +
'</a>' +
'<span class="separator-small"></span>' +
'<a class="item_market_action_button item_market_action_button_green relist_overpriced market_listing_button market_listing_button_right">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Relist overpriced</span>' +
'</a>' +
'<span class="separator-small"></span>' +
'<a class="item_market_action_button item_market_action_button_green select_overpriced market_listing_button market_listing_button_right">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Select overpriced</span>' +
'</a>' +
'</div>');
// Listings confirmations and buy orders.
$('.my_market_header').slice(1).append(
'<div class="market_listing_buttons">' +
'<a class="item_market_action_button item_market_action_button_green select_all market_listing_button">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Select all</span>' +
'</a>' +
'<span class="separator-large"></span>' +
'<a class="item_market_action_button item_market_action_button_green remove_selected market_listing_button">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Remove selected</span>' +
'</a>' +
'</div>');
$('.market_listing_table_header').on('click', 'span', function () {
if ($(this).hasClass('market_listing_edit_buttons') || $(this).hasClass('item_market_action_button_contents'))
return;
var isPrice = $('.market_listing_table_header', $(this).parent().parent()).children().eq(1).text() == $(this).text();
var isDate = $('.market_listing_table_header', $(this).parent().parent()).children().eq(2).text() == $(this).text();
var isName = $('.market_listing_table_header', $(this).parent().parent()).children().eq(3).text() == $(this).text();
sortMarketListings($(this).parent().parent(), isPrice, isDate, isName);
});
$('.select_all').on('click', '*', function () {
var selectionGroup = $(this).parent().parent().parent().parent();
var marketList = getListFromContainer(selectionGroup);
var invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length;
for (var i = 0; i < marketList.items.length; i++) {
$('.market_select_item', marketList.items[i].elm).prop('checked', !invert);
}
updateMarketSelectAllButton();
});
$('#market_removelisting_dialog_accept').on('click', '*', function () {
// This is when a user removed an item through the Remove/Cancel button.
// Ideally, it should remove this item from the list (instead of just the UI element which Steam does), but I'm not sure how to get the current item yet.
window.location.reload();
});
$('.select_overpriced').on('click', '*', function () {
var selectionGroup = $(this).parent().parent().parent().parent();
var marketList = getListFromContainer(selectionGroup);
for (var i = 0; i < marketList.items.length; i++) {
if ($(marketList.items[i].elm).hasClass('overpriced')) {
$('.market_select_item', marketList.items[i].elm).prop('checked', true);
}
}
$('.market_listing_row', selectionGroup).each(function (index) {
if ($(this).hasClass('overpriced'))
$('.market_select_item', $(this)).prop('checked', true);
});
updateMarketSelectAllButton();
});
$('.remove_selected').on('click', '*', function () {
var selectionGroup = $(this).parent().parent().parent().parent();
var marketList = getListFromContainer(selectionGroup);
for (var i = 0; i < marketList.items.length; i++) {
if ($('.market_select_item', $(marketList.items[i].elm)).prop('checked')) {
var listingid = replaceNonNumbers(marketList.items[i].values().market_listing_item_name);
marketRemoveQueue.push(listingid);
}
}
});
$('.market_relist_auto').change(function () {
setSetting(SETTING_RELIST_AUTOMATICALLY, $('.market_relist_auto').is(":checked") ? 1 : 0);
});
$('.relist_overpriced').on('click', '*', function () {
var selectionGroup = $(this).parent().parent().parent().parent();
var marketList = getListFromContainer(selectionGroup);
for (var i = 0; i < marketList.items.length; i++) {
if ($(marketList.items[i].elm).hasClass('overpriced')) {
var listingid = replaceNonNumbers(marketList.items[i].values().market_listing_item_name);
queueOverpricedItemListing(listingid);
}
}
});
$('.relist_selected').on('click', '*', function () {
var selectionGroup = $(this).parent().parent().parent().parent();
var marketList = getListFromContainer(selectionGroup);
for (var i = 0; i < marketList.items.length; i++) {
if ($(marketList.items[i].elm).hasClass('overpriced') && $('.market_select_item', $(marketList.items[i].elm)).prop('checked')) {
var listingid = replaceNonNumbers(marketList.items[i].values().market_listing_item_name);
queueOverpricedItemListing(listingid);
}
}
});
$('#see_settings').remove();
$('#global_action_menu').prepend('<span id="see_settings"><a href="javascript:void(0)">⬖ Steam Economy Enhancer</a></span>');
$('#see_settings').on('click', '*', () => openSettings());
processMarketListings();
}
}
//#endregion
//#region Tradeoffers
function sumTradeOfferAssets(assets, user) {
var total = {};
var totalPrice = 0;
for (var i = 0; i < assets.length; i++) {
var rgItem = user.findAsset(assets[i].appid, assets[i].contextid, assets[i].assetid);
var text = '';
if (rgItem != null) {
if (rgItem.element) {
var inventoryPriceElements = $('.inventory_item_price', rgItem.element);
if (inventoryPriceElements.length) {
var firstPriceElement = inventoryPriceElements[0];
var classes = $(firstPriceElement).attr('class').split(' ');
for (var c in classes) {
if (classes[c].toString().includes('price_')) {
var price = parseInt(classes[c].toString().replace('price_', ''));
totalPrice += price;
}
}
}
}
if (rgItem.original_amount != null && rgItem.amount != null) {
var originalAmount = parseInt(rgItem.original_amount);
var currentAmount = parseInt(rgItem.amount);
var usedAmount = originalAmount - currentAmount;
text += usedAmount.toString() + 'x ';
}
text += rgItem.name;
if (rgItem.type != null && rgItem.type.length > 0) {
text += ' (' + rgItem.type + ')';
}
}
else
text = 'Unknown Item';
if (text in total)
total[text] = total[text] + 1;
else
total[text] = 1;
}
var sortable = [];
for (var item in total)
sortable.push([item, total[item]])
sortable.sort(function (a, b) {
return a[1] - b[1];
}).reverse();
var totalText = '<strong>Number of items: ' + sortable.length + ', worth ' + (totalPrice / 100).toFixed(2) + currencySymbol + '<br/><br/></strong>';
for (var i = 0; i < sortable.length; i++) {
totalText += sortable[i][1] + 'x ' + sortable[i][0] + '<br/>';
}
return totalText;
}
function initializeTradeOfferUI() {
$('.trade_right > div > div > div > .trade_item_box').observe('childlist subtree', function (record) {
$('#trade_offer_your_sum').remove();
$('#trade_offer_their_sum').remove();
var your_sum = sumTradeOfferAssets(g_rgCurrentTradeStatus.me.assets, UserYou);
var their_sum = sumTradeOfferAssets(g_rgCurrentTradeStatus.them.assets, UserThem);
$('div.offerheader:nth-child(1) > div:nth-child(3)').append('<div class="trade_offer_sum" id="trade_offer_your_sum">' + your_sum + '</div>');
$('div.offerheader:nth-child(3) > div:nth-child(3)').append('<div class="trade_offer_sum" id="trade_offer_their_sum">' + their_sum + '</div>');
});
// This only works with a new trade offer.
if (!window.location.href.includes('tradeoffer/new'))
return;
var updateInventoryPrices = function () {
var tradeOfferItems = [];
for (var i = 0; i < getActiveInventory().rgItemElements.length; i++) {
tradeOfferItems.push(getActiveInventory().rgItemElements[i].rgItem);
}
setInventoryPrices(tradeOfferItems);
}
$('#inventory_pagecontrols').observe('childlist', '*', function (record) {
updateInventoryPrices();
});
$('#inventory_displaycontrols').append(
'<br/>' +
'<div class="trade_offer_buttons">' +
'<a class="item_market_action_button item_market_action_button_green select_all" style="margin-top:1px">' +
'<span class="item_market_action_button_contents" style="text-transform:none">Select all from page</span>' +
'</a>' +
'</div>');
$('.select_all').on('click', '*', function () {
$('.inventory_ctn:visible > .inventory_page:visible > .itemHolder:visible').delayedEach(250, function (i, it) {
var item = it.rgItem;
if (item.is_stackable)
return;
if (!item.tradable)
return;
MoveItemToTrade(it);
});
});
}
//#endregion
//#region Settings
function openSettings() {
var price_options = $('<div id="price_options">' +
'<div style="margin-bottom:6px;">' +
'Считать цены как:&nbsp;<select class="price_option_input" style="background-color: black;color: white;border: transparent;" id="' + SETTING_PRICE_ALGORITHM + '">' +
'<option value="1"' + (getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1 ? 'selected="selected"' : '') + '>максимум от среднего (12 часов) и минимальная цена</option>' +
'<option value="2" ' + (getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 2 ? 'selected="selected"' : '') + '>минимальная цена</option>' +
'</select>' +
'<br/>' +
'</div>' +
'<div style="margin-bottom:6px;">' +
'Значение для добавления к расчетной цене (минимальное и максимальное соблюдаются):&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_PRICE_OFFSET + '" value=' + getSettingWithDefault(SETTING_PRICE_OFFSET) + '>' +
'<br/>' +
'</div>' +
'<div style="margin-top:6px">' +
'Использовать вторую самую низкую цену, когда по самой низкой цене выставлено мало предметов:&nbsp;<input class="price_option_input" style="background-color: black;color: white;border: transparent;" type="checkbox" id="' + SETTING_PRICE_IGNORE_LOWEST_Q + '" ' + (getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1 ? 'checked=""' : '') + '>' +
'<br/>' +
'</div>' +
'<div style="margin-top:24px">' +
'<div style="margin-bottom:6px;">' +
'Минимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MIN_NORMAL_PRICE + '" value=' + getSettingWithDefault(SETTING_MIN_NORMAL_PRICE) + '>&nbsp;' +
'и максимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MAX_NORMAL_PRICE + '" value=' + getSettingWithDefault(SETTING_MAX_NORMAL_PRICE) + '>&nbsp;цен на обычные карты' +
'<br/>' +
'</div>' +
'<div style="margin-bottom:6px;">' +
'Минимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MIN_FOIL_PRICE + '" value=' + getSettingWithDefault(SETTING_MIN_FOIL_PRICE) + '>&nbsp;' +
'и максимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MAX_FOIL_PRICE + '" value=' + getSettingWithDefault(SETTING_MAX_FOIL_PRICE) + '>&nbsp;цен на металлические карты' +
'<br/>' +
'</div>' +
'<div style="margin-bottom:6px;">' +
'Минимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MIN_MISC_PRICE + '" value=' + getSettingWithDefault(SETTING_MIN_MISC_PRICE) + '>&nbsp;' +
'и максимум:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MAX_MISC_PRICE + '" value=' + getSettingWithDefault(SETTING_MAX_MISC_PRICE) + '>&nbsp;цен на другие предметы' +
'<br/>' +
'</div>' +
'<div style="margin-top:24px;margin-bottom:6px;">' +
'Количество предметов на страницу:&nbsp;<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_MARKET_PAGE_COUNT + '" value=' + getSettingWithDefault(SETTING_MARKET_PAGE_COUNT) + '>' +
'<br/>' +
'<div style="margin-top:6px;">' +
'Автоматически пересчитывать завышенные цены в маркете (медленно на больших инвентарях):&nbsp;<input id="' + SETTING_RELIST_AUTOMATICALLY + '" class="market_relist_auto" type="checkbox" ' + (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1 ? 'checked=""' : '') + '>' +
'</label>' +
'</div>' +
'</div>' +
'</div>');
var dialog = ShowConfirmDialog('Steam Economy Enhancer RUS', price_options).done(function () {
setSetting(SETTING_MIN_NORMAL_PRICE, $('#' + SETTING_MIN_NORMAL_PRICE, price_options).val());
setSetting(SETTING_MAX_NORMAL_PRICE, $('#' + SETTING_MAX_NORMAL_PRICE, price_options).val());
setSetting(SETTING_MIN_FOIL_PRICE, $('#' + SETTING_MIN_FOIL_PRICE, price_options).val());
setSetting(SETTING_MAX_FOIL_PRICE, $('#' + SETTING_MAX_FOIL_PRICE, price_options).val());
setSetting(SETTING_MIN_MISC_PRICE, $('#' + SETTING_MIN_MISC_PRICE, price_options).val());
setSetting(SETTING_MAX_MISC_PRICE, $('#' + SETTING_MAX_MISC_PRICE, price_options).val());
setSetting(SETTING_PRICE_OFFSET, $('#' + SETTING_PRICE_OFFSET, price_options).val());
setSetting(SETTING_PRICE_ALGORITHM, $('#' + SETTING_PRICE_ALGORITHM, price_options).val());
setSetting(SETTING_MARKET_PAGE_COUNT, $('#' + SETTING_MARKET_PAGE_COUNT, price_options).val());
setSetting(SETTING_RELIST_AUTOMATICALLY, $('#' + SETTING_RELIST_AUTOMATICALLY, price_options).prop('checked') ? 1 : 0);
setSetting(SETTING_PRICE_IGNORE_LOWEST_Q, $('#' + SETTING_PRICE_IGNORE_LOWEST_Q, price_options).prop('checked') ? 1 : 0);
window.location.reload();
});
}
//#endregion
//#region UI
injectCss('.ui-selected { outline: 1px groove #FFFFFF; } ' +
'#logger { color: #767676; font-size: 12px;margin-top:16px; max-height: 200px; overflow-y: auto; }' +
'.trade_offer_sum { color: #767676; font-size: 12px;margin-top:8px; }' +
'.trade_offer_buttons { margin-top: 12px; }' +
'.market_commodity_orders_table { font-size:12px; font-family: "Motiva Sans", Sans-serif; font-weight: 300; }' +
'.market_commodity_orders_table th { padding-left: 10px; }' +
'#listings_group { display: flex; justify-content: space-between; margin-bottom: 8px; }' +
'#listings_sell { text-align: right; color: #589328; font-weight:600; }' +
'#listings_buy { text-align: right; color: #589328; font-weight:600; }' +
'.market_listing_my_price { height: 50px; padding-right:6px; }' +
'.market_listing_edit_buttons.actual_content { width:276px; transition-property: background-color, border-color; transition-timing-function: linear; transition-duration: 0.5s;}' +
'.market_listing_buttons { margin-top: 6px; background: rgba(0, 0, 0, 0.4); padding: 5px 0px 1px 0px; }' +
'.market_listing_button { margin-right: 4px; }' +
'.market_listing_button_right { float:right; }' +
'.market_listing_button:first-child { margin-left: 4px; }' +
'.market_listing_label_right { float:right; font-size:12px; margin-top:1px; }' +
'.market_listing_select { position: absolute; top: 16px;right: 10px; display: flex; }' +
'#market_listing_relist { vertical-align: middle; position: relative; bottom: -1px; right: 2px; }' +
'.pick_and_sell_button > a { vertical-align: middle; }' +
'.market_relist_auto { margin-bottom: 8px; }' +
'.market_relist_auto_label { margin-right: 6px; }' +
'.quick_sell { margin-right: 4px; }' +
'.spinner{margin:10px auto;width:50px;height:40px;text-align:center;font-size:10px;}.spinner > div{background-color:#ccc;height:100%;width:6px;display:inline-block;-webkit-animation:sk-stretchdelay 1.2s infinite ease-in-out;animation:sk-stretchdelay 1.2s infinite ease-in-out}.spinner .rect2{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner .rect3{-webkit-animation-delay:-1s;animation-delay:-1s}.spinner .rect4{-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner .rect5{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes sk-stretchdelay{0%,40%,100%{-webkit-transform:scaleY(0.4)}20%{-webkit-transform:scaleY(1.0)}}@keyframes sk-stretchdelay{0%,40%,100%{transform:scaleY(0.4);-webkit-transform:scaleY(0.4)}20%{transform:scaleY(1.0);-webkit-transform:scaleY(1.0)}}' +
'#market_name_search { float: right; background: rgba(0, 0, 0, 0.25); color: white; border: none;height: 25px; padding-left: 6px;}' +
'.price_option_price { width: 100px }' +
'#see_settings { background: #26566c; margin-right: 10px; height: 21px; line-height:21px; display:inline-block; padding: 0px 6px; }' +
'.inventory_item_price { top: 0px;position: absolute;right: 0;background: #3571a5;padding: 2px;color: white; font-size:11px; border: 1px solid #666666;}' +
'.separator-large {display:inline-block;width:6px;}' +
'.separator-small {display:inline-block;width:1px;}' +
'.pagination { padding-left: 0px; }' +
'.pagination li { display:inline-block; padding: 5px 10px;background: rgba(255, 255, 255, 0.10); margin-right: 6px; border: 1px solid #666666; }' +
'.pagination li.active { background: rgba(255, 255, 255, 0.25); }');
$(document).ready(function () {
// Make sure the user is logged in, there's not much we can do otherwise.
if (!isLoggedIn) {
return;
}
if (currentPage == PAGE_INVENTORY) {
initializeInventoryUI();
}
if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) {
initializeMarketUI();
}
if (currentPage == PAGE_TRADEOFFER) {
initializeTradeOfferUI();
}
});
function injectCss(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) {
return;
}
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
function injectJs(js) {
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.textContent = '(' + js + ')();';
document.body.appendChild(script);
document.body.removeChild(script);
}
$.fn.delayedEach = function (timeout, callback, continuous) {
var $els, iterator;
$els = this;
iterator = function (index) {
var cur;
if (index >= $els.length) {
if (!continuous) {
return;
}
index = 0;
}
cur = $els[index];
callback.call(cur, index, cur);
setTimeout(function () {
iterator(++index);
}, timeout);
};
iterator(0);
};
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
//#endregion
})(jQuery, async);
@Haoose
Copy link
Author

Haoose commented Sep 8, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment