您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增强 Steam 库存和 Steam 市场功能
// ==UserScript== // @name Steam Economy Enhancer // @icon https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg // @namespace https://github.com/Nuklon // @author Nuklon // @license MIT // @version 6.8.6 // @description 增强 Steam 库存和 Steam 市场功能 // @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.3.1.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js // @require https://unpkg.com/@kapetan/[email protected]/jquery-observe.js // @require https://unpkg.com/[email protected]/dist/pagination.min.js // @require https://unpkg.com/[email protected]/dist/async.min.js // @require https://unpkg.com/[email protected]/dist/localforage.min.js // @require https://unpkg.com/[email protected]/build/global/luxon.min.js // @require https://unpkg.com/[email protected]/dist/list.min.js // @require https://unpkg.com/[email protected]/dist/jquery.checkboxes-1.2.2.min.js // @grant unsafeWindow // @grant GM_addStyle // @homepageURL https://steamcn.com/t311996-1-1 // @supportURL https://steamcn.com/t311996-1-1 // ==/UserScript== // jQuery is already added by Steam, force no conflict mode. (function($, async) { $.noConflict(true); var DateTime = luxon.DateTime; 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 COLOR_PRICE_NOT_CHECKED = '#26566c'; const ERROR_SUCCESS = null; const ERROR_FAILED = 1; const ERROR_DATA = 2; var marketLists = []; var totalNumberOfProcessedQueueItems = 0; var totalNumberOfQueuedItems = 0; var totalPriceWithFeesOnMarket = 0; var totalPriceWithoutFeesOnMarket = 0; var totalScrap = 0; var spinnerBlock = '<div class="spinner"><div class="rect1"></div> <div class="rect2"></div> <div class="rect3"></div> <div class="rect4"></div> <div class="rect5"></div> </div>'; var numberOfFailedRequests = 0; var enableConsoleLog = false; var country = typeof unsafeWindow.g_strCountryCode !== 'undefined' ? unsafeWindow.g_strCountryCode : undefined; var isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || (typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.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(unsafeWindow.g_rgAppContextData, typeof unsafeWindow.g_strInventoryLoadURL !== 'undefined' && unsafeWindow.g_strInventoryLoadURL != null ? unsafeWindow.g_strInventoryLoadURL : typeof unsafeWindow.g_strProfileURL !== 'undefined' && unsafeWindow.g_strProfileURL != null ? unsafeWindow.g_strProfileURL + '/inventory/json/' : 'https://steamcommunity.com/my/inventory/json/', isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined); var currencyId = isLoggedIn && market != null && market.walletInfo != null && market.walletInfo.wallet_currency != null ? market.walletInfo.wallet_currency : 3; var currencySymbol = unsafeWindow.GetCurrencySymbol(unsafeWindow.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_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE'; const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM'; const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q'; const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS'; const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS'; const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS'; const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE'; const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY'; const SETTING_MARKET_PAGE_COUNT = 'SETTING_MARKET_PAGE_COUNT'; const SETTING_INVENTORY_PRICES = 'SETTING_INVENTORY_PRICES'; 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.00, SETTING_PRICE_MIN_CHECK_PRICE: 0.00, SETTING_PRICE_ALGORITHM: 1, SETTING_PRICE_IGNORE_LOWEST_Q: 1, SETTING_PRICE_HISTORY_HOURS: 12, SETTING_INVENTORY_PRICE_LABELS: 1, SETTING_TRADEOFFER_PRICE_LABELS: 1, SETTING_LAST_CACHE: 0, SETTING_RELIST_AUTOMATICALLY: 0, SETTING_MARKET_PAGE_COUNT: 100 }; GM_addStyle('.inventory_iteminfo .market_commodity_orders_table th {min-width: 69px; padding: 4px} .inventory_iteminfo .market_commodity_orders_table td {min-width: initial} @media screen and (max-width: 910px) {html.responsive .view_inventory_logo {max-height: unset !important;}}'); 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('无法设置localStorage内容,名称:' + 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('无法设置sessionStorage内容,名称:' + 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 xx hours. var timeAgo = Date.now() - (getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 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 (typeof histogram === 'undefined' || 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; } function calculateBuyOrderPriceBeforeFees(histogram) { if (typeof histogram === 'undefined') return 0; return market.getPriceBeforeFees(histogram.highest_buy_order); } // 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 buyPrice = calculateBuyOrderPriceBeforeFees(histogram); var shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1; var shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3; // 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 (shouldUseBuyOrder && buyPrice !== -2) { calculatedPrice = buyPrice; } else 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 (typeof histogram !== 'undefined' && 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) { if (data.success === false && isRetryMessage(data.message)) { callback(ERROR_FAILED, data); } else { 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 { for (var i = 0; i < item.owner_actions.length; i++) { var action = item.owner_actions[i]; if ( !action.link || !action.name ) { continue; } if (action.link.match(/^javascript:GetGooValue/)) { var item_data = action.link.split(','); var appid = item_data[2].trim(); var item_type = item_data[3].trim(); var border_color = item_data[4].split(' ')[0].trim(); $.ajax({ type: "GET", url: window.location.protocol+'//steamcommunity.com/auction/ajaxgetgoovalueforitemtype/', data: { appid: appid, item_type: item_type, border_color: border_color }, 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/ } // Unpacks the booster pack. SteamMarket.prototype.unpackBoosterPack = function(item, callback) { try { var sessionId = readCookie('sessionid'); $.ajax({ type: "POST", url: this.inventoryUrlBase + 'ajaxunpackbooster/', data: { sessionid: sessionId, appid: item.market_fee_app, communityitemid: item.assetid }, 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 //communityitemid = 4830605461 //http://steamcommunity.com/my/ajaxunpackbooster/ } // 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?country=' + country + '&language=schinese¤cy=' + 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 getIsCrate(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 isTaggedAsCrate = false; tags.forEach(function (arrayItem) { if (arrayItem.category == 'Type') if (arrayItem.internal_name == 'Supply Crate') isTaggedAsCrate = true; }); if (isTaggedAsCrate) return true; } } 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; } function isRetryMessage(message) { var messageList = [ "在上一个操作完成之前,您不能出售任何物品。", "列出您的物品时出现问题。刷新页面并重试。", "我们无法连接到游戏物品服务器。游戏物品服务器可能已经关闭,或 Steam 可能正面临临时连接问题。您的列表尚未创建。请刷新页面并重试。" ]; return messageList.indexOf(message) !== -1; } //#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) { function onQueueDrain() { if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0 && boosterQueue.length() == 0) { $('#inventory_items_spinner').remove(); } } function updateTotals() { if ($('#loggerTotal').length == 0) { $(logger).parent().append('<div id="loggerTotal"></div>'); } var totals = document.getElementById('loggerTotal'); totals.innerHTML = ''; if (totalPriceWithFeesOnMarket > 0) { totals.innerHTML += '<div><strong>累计上架物品总价为 ' + (totalPriceWithFeesOnMarket / 100.0).toFixed(2) + currencySymbol + ', 你将会获得 ' + (totalPriceWithoutFeesOnMarket / 100).toFixed(2) + currencySymbol + '.</strong></div>'; } if (totalScrap > 0) { totals.innerHTML += '<div><strong>总共分解:' + totalScrap + '</strong></div>'; } } 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 + ' 已添加至市场,售价为 ' + (market.getPriceIncludingFees(task.sellPrice) / 100.0).toFixed(2) + currencySymbol + ', 你将收到 ' + (task.sellPrice / 100.0).toFixed(2) + currencySymbol + '.'); $('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId) .css('background', COLOR_SUCCESS); totalPriceWithoutFeesOnMarket += task.sellPrice; totalPriceWithFeesOnMarket += market.getPriceIncludingFees(task.sellPrice); updateTotals(); } else if (data != null && isRetryMessage(data.message)) { logDOM(padLeft + ' - ' + itemName + ' 正在重试列出物品,原因为 ' + data.message[0].toLowerCase() + data.message.slice(1)); totalNumberOfProcessedQueueItems--; sellQueue.unshift(task); sellQueue.pause(); setTimeout(function() { sellQueue.resume(); }, getRandomInt(30000, 45000)); } else { if (data != null && data.responseJSON != null && data.responseJSON.message != null) { logDOM(padLeft + ' - ' + itemName + ' 上架市场失败,原因为 ' + data.responseJSON.message[0].toLowerCase() + data.responseJSON.message.slice(1)); } else logDOM(padLeft + ' - ' + itemName + ' 上架市场失败'); $('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId) .css('background', COLOR_ERROR); } next(); }); }, 1); sellQueue.drain = function() { onQueueDrain(); } 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('无法检索库存...'); }); } function sellAllDuplicateItems() { loadAllInventories().then(function () { var items = getInventoryItems(); var marketableItems = []; var filteredItems = []; items.forEach(function (item) { if (!item.marketable) { return; } marketableItems.push(item); }); filteredItems = marketableItems.filter((e, i) => marketableItems.map(m => m.classid).indexOf(e.classid) !== i); sellItems(filteredItems); }, function () { logDOM('无法检索库存...'); }); } 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('无法检索库存...'); }); } function sellAllCrates() { loadAllInventories().then(function () { var items = getInventoryItems(); var filteredItems = []; items.forEach(function (item) { if (!getIsCrate(item) || !item.marketable) { return; } filteredItems.push(item); }); sellItems(filteredItems); }, function() { logDOM('无法检索库存...'); }); } 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() { onQueueDrain(); } 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('无法获取 ' + itemName + ' 分解后的宝石数。'); logDOM(padLeft + ' - ' + itemName + ' 由于缺少宝石数而未变成宝石'); $('#' + 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('无法将' + itemName + '分解为宝石。'); logDOM(padLeft + ' - ' + itemName + ' 由于未知错误,未分解为宝石'); $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR); return callback(false); } logConsole('============================') logConsole(itemName); logConsole('分解为 ' + goo.goo_value + '个 宝石'); logDOM(padLeft + ' - ' + itemName + ' 已分解为 ' + item.goo_value_expected + '个 宝石.'); $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_SUCCESS); totalScrap += item.goo_value_expected; updateTotals(); callback(true); }); }); } var boosterQueue = async.queue(function(item, next) { boosterQueueWorker(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); boosterQueue.drain = function() { onQueueDrain(); } function boosterQueueWorker(item, callback) { var failed = 0; var itemName = item.name || item.description.name; var itemId = item.assetid || item.id; market.unpackBoosterPack(item, function(err, goo) { totalNumberOfProcessedQueueItems++; var digits = getNumberOfDigits(totalNumberOfQueuedItems); var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems; if (err != ERROR_SUCCESS) { logConsole('无法拆开补充包 ' + itemName); logDOM(padLeft + ' - ' + itemName + ' 拆包失败.'); $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR); return callback(false); } logDOM(padLeft + ' - ' + itemName + ' 拆包成功.'); $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_SUCCESS); callback(true); }); } // Turns the selected items into gems. function turnSelectedItemsIntoGems() { var ids = getSelectedItems(); 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">正在处理 ' + numberOfQueuedItems + '个 物品</div>' + '</div>'); } }, function() { logDOM('无法检索库存...'); }); } // Unpacks the selected booster packs. function unpackSelectedBoosterPacks() { var ids = getSelectedItems(); 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 canOpenBooster = false; for (var owner_action in item.owner_actions) { if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { canOpenBooster = true; } } if (!canOpenBooster) return; var itemId = item.assetid || item.id; if (ids.indexOf(itemId) !== -1) { item.queued = true; boosterQueue.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">正在处理 ' + numberOfQueuedItems + '个 物品</div>' + '</div>'); } }, function() { logDOM('无法检索库存...'); }); } function sellSelectedItems() { getInventorySelectedMarketableItems(function(items) { sellItems(items); }); } function canSellSelectedItemsManually(items) { // We have to construct an URL like this // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 var appid = items[0].appid; var contextid = items[0].contextid; var hasInvalidItem = false; items.forEach(function(item) { if (item.contextid != contextid || item.commodity == false) hasInvalidItem = true; }); return !hasInvalidItem; } function sellSelectedItemsManually() { getInventorySelectedMarketableItems(function(items) { // We have to construct an URL like this // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 var appid = items[0].appid; var contextid = items[0].contextid; var itemsWithQty = {}; items.forEach(function(item) { itemsWithQty[item.market_hash_name] = itemsWithQty[item.market_hash_name] + 1 || 1; }); var itemsString = ''; for (var itemName in itemsWithQty) { itemsString += '&items[]=' + encodeURI(itemName) + '&qty[]=' + itemsWithQty[itemName]; } var baseUrl = 'https://steamcommunity.com/market/multisell'; var redirectUrl = baseUrl + '?appid=' + appid + '&contextid=' + contextid + itemsString; var dialog = unsafeWindow.ShowDialog('Steam Economy Enhancer', '<iframe frameBorder="0" height="650" width="900" src="' + redirectUrl + '"></iframe>'); dialog.OnDismiss(function() { items.forEach(function(item) { var itemId = item.assetid || item.id; $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_PENDING); }); }); }); } function sellItems(items) { if (items.length == 0) { logDOM('这些物品无法被上架至市场...'); 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">正在处理 ' + numberOfQueuedItems + '个 物品</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('无法获取 ' + itemName + ' 的价格历史'); if (err == ERROR_FAILED) failed += 1; } market.getItemOrdersHistogram(item, true, function(err, histogram, cachedListings) { if (err) { logConsole('无法获取 ' + 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('售价:' + 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 = unsafeWindow.g_ActiveUser.strSteamId == unsafeWindow.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; // Steam adds 'display:none' to items while searching. These should not be selected while using shift/ctrl. var filter = ".itemHolder:not([style*=none])"; $('#inventories').selectable({ filter: filter, selecting: function(e, ui) { // Get selected item index. var selectedIndex = $(ui.selecting.tagName, e.target).index(ui.selecting); // If shift key was pressed and there is previous - select them all. if (e.shiftKey && previousSelection > -1) { $(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); } }); } // Gets the selected items in the inventory. function getSelectedItems() { 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]); } }); }); }); }); return ids; } // Gets the selected and marketable items in the inventory. function getInventorySelectedMarketableItems(callback) { var ids = getSelectedItems(); 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); } }); callback(filteredItems); }, function() { logDOM('无法检索库存...'); }); } // Gets the selected and gemmable items in the inventory. function getInventorySelectedGemsItems(callback) { var ids = getSelectedItems(); loadAllInventories().then(function() { var items = getInventoryItems(); var filteredItems = []; items.forEach(function(item) { 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) { filteredItems.push(item); } }); callback(filteredItems); }, function() { logDOM('无法检索库存...'); }); } // Gets the selected and booster pack items in the inventory. function getInventorySelectedBoosterPackItems(callback) { var ids = getSelectedItems(); loadAllInventories().then(function() { var items = getInventoryItems(); var filteredItems = []; items.forEach(function(item) { var canOpenBooster = false; for (var owner_action in item.owner_actions) { if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { canOpenBooster = true; } } if (!canOpenBooster) return; var itemId = item.assetid || item.id; if (ids.indexOf(itemId) !== -1) { filteredItems.push(item); } }); callback(filteredItems); }, function() { logDOM('无法检索库存...'); }); } // Updates the (selected) sell ... items button. function updateSellSelectedButton() { getInventorySelectedMarketableItems(function(items) { var selectedItems = items.length; if (items.length == 0) { $('.sell_selected').hide(); $('.sell_manual').hide(); } else { $('.sell_selected').show(); if (canSellSelectedItemsManually(items)) { $('.sell_manual').show(); $('.sell_manual > span').text('手动出售 ' + selectedItems + '个 物品'); } else { $('.sell_manual').hide(); } $('.sell_selected > span').text('出售 ' + selectedItems + '个 物品'); } }); } // Updates the (selected) turn into ... gems button. function updateTurnIntoGemsButton() { getInventorySelectedGemsItems(function(items) { var selectedItems = items.length; if (items.length == 0) { $('.turn_into_gems').hide(); } else { $('.turn_into_gems').show(); $('.turn_into_gems > span') .text('分解 ' + selectedItems + '个 物品为宝石'); } }); } // Updates the (selected) open ... booster packs button. function updateOpenBoosterPacksButton() { getInventorySelectedBoosterPackItems(function(items) { var selectedItems = items.length; if (items.length == 0) { $('.unpack_booster_packs').hide(); } else { $('.unpack_booster_packs').show(); $('.unpack_booster_packs > span') .text('拆开 ' + selectedItems + '个 补充包'); } }); } function updateInventorySelection(item) { updateSellSelectedButton(); updateTurnIntoGemsButton(); updateOpenBoosterPacksButton(); // 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('无法获取 ' + (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); var ownerActions = $('#' + item_info_id + '_item_owner_actions'); // ownerActions is hidden on other games' inventories, we need to show it to have a "Market" button visible ownerActions.show(); ownerActions.append('<a class="btn_small btn_grey_white_innerfade" href="/market/listings/' + appid + '/' + market_hash_name + '"><span>在社区市场中查看</span></a>'); $('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(1)').hide(); var isBoosterPack = getActiveInventory().selectedItem.name.toLowerCase().endsWith('booster pack'); if (isBoosterPack) { var tradingCardsUrl = "/market/search?q=&category_753_Game%5B%5D=tag_app_" + getActiveInventory().selectedItem.market_fee_app + "&category_753_item_class%5B%5D=tag_item_class_2&appid=753"; ownerActions.append('<br/> <a class="btn_small btn_grey_white_innerfade" href="' + tradingCardsUrl + '"><span>在社区市场中查看可集换式卡牌</span></a>'); } // 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) { // Transaction volume must be separable into three or more parts (no matter if equal): valve+publisher+seller. if (parseInt(histogram.lowest_sell_order) > 3) { prices.push(parseInt(histogram.lowest_sell_order) - 1); } prices.push(parseInt(histogram.lowest_sell_order)); } prices = prices.filter((v, i) => prices.indexOf(v) === i).sort((a, b) => a - b); var buttons = '<div>'; var oributton = $('#' + item_info_id + '_item_market_actions > a.item_market_action_button', item_info); buttons += '<a class="item_market_action_button item_market_action_button_green" style="margin-right: 4px; display: inline-block;" href="javascript:SellCurrentSelection()">'+oributton.html()+'</a>'; oributton.remove(); prices.forEach(function(e) { buttons += '<a class="item_market_action_button item_market_action_button_green quick_sell" style="display: inline-block;" 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>' }); buttons += '</div>'; $('#' + item_info_id + '_item_market_actions', item_info).append(buttons); $('#' + item_info_id + '_item_market_actions', item_info).append( '<div style="display:flex">' + '<input id="quick_sell_input" style="background-color: black;color: white;border: transparent;max-width:65px;text-align:center;" type="number" value="' + (histogram.lowest_sell_order / 100) + '" step="0.01" />' + ' <a class="item_market_action_button item_market_action_button_green quick_sell_custom">' + '<span class="item_market_action_button_edge item_market_action_button_left"></span>' + '<span class="item_market_action_button_contents">➜ 确认出售</span>' + '<span class="item_market_action_button_edge item_market_action_button_right"></span>' + '<span class="item_market_action_button_preload"></span>' + '</a>' + '</div>'); $('.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 }); }); $('.quick_sell_custom').on('click', function() { var price = $('#quick_sell_input', $('#' + item_info_id + '_item_market_actions', item_info)).val() * 100; 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)">⬖ SEE 设置 </a></span>'); $('#see_settings').on('click', '*', () => openSettings()); var appId = getActiveInventory().m_appid; var showMiscOptions = appId == 753; var TF2 = appId == 440; var sellButtons = $('<div id="inventory_sell_buttons" style="margin-bottom:12px;">' + '<a class="btn_green_white_innerfade btn_medium_wide sell_all separator-btn-right"><span>出售所有物品</span></a>' + '<a class="btn_green_white_innerfade btn_medium_wide sell_all_duplicates separator-btn-right"><span>出售所有重复物品</span></a>' + '<a class="btn_green_white_innerfade btn_medium_wide sell_selected separator-btn-right" style="display:none"><span>出售所选物品</span></a>' + '<a class="btn_green_white_innerfade btn_medium_wide sell_manual separator-btn-right" style="display:none"><span>手动出售物品</span></a>' + (showMiscOptions ? '<a class="btn_green_white_innerfade btn_medium_wide sell_all_cards separator-btn-right"><span>出售所有卡牌</span></a>' + '<div style="margin-top:12px;">' + '<a class="btn_darkblue_white_innerfade btn_medium_wide turn_into_gems separator-btn-right" style="display:none"><span>将选中物品分解为宝石</span></a>' + '<a class="btn_darkblue_white_innerfade btn_medium_wide unpack_booster_packs separator-btn-right" style="display:none"><span>拆开选中的补充包</span></a>' + '</div>' : '') + (TF2 ? '<a class="btn_green_white_innerfade btn_medium_wide sell_all_crates separator-btn-right"><span>出售所有箱子</span></a>' : '') + '</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_duplicates').on('click', '*', sellAllDuplicateItems); $('.sell_manual').on('click', '*', sellSelectedItemsManually); $('.sell_all_cards').on('click', '*', sellAllCards); $('.sell_all_crates').on('click', '*', sellAllCrates); $('.turn_into_gems').on('click', '*', turnSelectedItemsIntoGems); $('.unpack_booster_packs').on('click', '*', unpackSelectedBoosterPacks); } $('.inventory_rightnav').prepend(reloadButton); $('.reload_inventory').on('click', '*', function() { window.location.reload(); }); loadAllInventories().then(function() { var updateInventoryPrices = function() { if (getSettingWithDefault(SETTING_INVENTORY_PRICE_LABELS) == 1) { setInventoryPrices(getInventoryItems()); } }; // Load after the inventory is loaded. updateInventoryPrices(); $('#inventory_pagecontrols').observe('childlist', '*', function(record) { updateInventoryPrices(); }); }, function() { logDOM('无法检索库存...'); }); } // 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 unsafeWindow.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('无法获取 ' + 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 marketListingsRelistedAssets = []; 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; }) }; // Gets the price, in cents, from a market listing. function getPriceFromMarketListing(listing) { var priceLabel = listing.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'; } return parseInt(replaceNonNumbers(priceLabel)); } function marketListingsQueueWorker(listing, ignoreErrors, callback) { var asset = unsafeWindow.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 price = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listingUI).text()); if (price <= getSettingWithDefault(SETTING_PRICE_MIN_CHECK_PRICE) * 100) { $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_NOT_CHECKED); $('.market_listing_my_price', listingUI).last().prop('title', 'The price is not checked.'); listingUI.addClass('not_checked'); return callback(true, true); } 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('无法获取 ' + game_name + ' 的价格历史'); if (errorPriceHistory == ERROR_FAILED) failed += 1; } market.getItemOrdersHistogram(item, true, function(errorHistogram, histogram, cachedListings) { if (errorHistogram) { logConsole('无法获取 '+ 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="这可能是当前最高买价。">' + highestBuyOrderPrice + '</span>'); logConsole('============================') logConsole(JSON.stringify(listing)); logConsole(game_name + ': ' + asset.name); logConsole('当前价格:' + 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('计算出的价格:' + sellPriceWithoutOffsetWithFees / 100.0 + ' (' + sellPriceWithoutOffset / 100.0 + ')'); listingUI.addClass('price_' + sellPriceWithOffset); $('.market_listing_my_price', listingUI).last().prop('title', '最好的价格是 ' + (sellPriceWithoutOffsetWithFees / 100.0) + currencySymbol + '.'); if (sellPriceWithoutOffsetWithFees < price) { logConsole('售价太高。'); $('.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('售价太低。'); $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_CHEAP); listingUI.addClass('underpriced'); } else { logConsole('售价正好。'); $('.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; unsafeWindow.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 (marketListingsRelistedAssets.indexOf(child) == -1 && inventory[child].appid == item.appid && (inventory[child].market_hash_name == decodedMarketHashName || inventory[child].market_hash_name == marketHashName)) { newAssetId = child; break; } } if (newAssetId == -1) { $('.actual_content', listingUI).css('background', COLOR_ERROR); return callback(false); } item.assetid = newAssetId; marketListingsRelistedAssets.push(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 unsafeWindow.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); }); var totalPriceBuyer = 0; var totalPriceSeller = 0; // 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); if (!isNaN(assetInfo.priceBuyer)) totalPriceBuyer += assetInfo.priceBuyer; if (!isNaN(assetInfo.priceSeller)) totalPriceSeller += assetInfo.priceSeller; marketListingsQueue.push({ listingid, appid: assetInfo.appid, contextid: assetInfo.contextid, assetid: assetInfo.assetid }); } } $('#my_market_selllistings_number').append('<span id="my_market_sellistings_total_price">, ' + (totalPriceBuyer / 100.0).toFixed(2) + currencySymbol + ' ➤ ' + (totalPriceSeller / 100.0).toFixed(2) + currencySymbol + '</span>'); } // 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 priceBuyer = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listing.elm).text()); var priceSeller = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(3)', listing.elm).text()); var itemIds = actionButton.split(','); var appid = replaceNonNumbers(itemIds[2]); var contextid = replaceNonNumbers(itemIds[3]); var assetid = replaceNonNumbers(itemIds[4]); return { appid, contextid, assetid, priceBuyer, priceSeller }; } // 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); list.on('searchComplete', updateMarketSelectAllButton); 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 unsafeWindow.g_oMyListings !== 'undefined' && unsafeWindow.g_oMyListings != null && unsafeWindow.g_oMyListings.m_cTotalCount != null) totalCount = unsafeWindow.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">正在加载交易列表...</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 unsafeWindow.g_rgAssets) { for (var contextid in unsafeWindow.g_rgAssets[appid]) { for (var assetid in unsafeWindow.g_rgAssets[appid][contextid]) { existingAsset = unsafeWindow.g_rgAssets[appid][contextid][assetid]; break; } } } // appid and contextid are identical, only the assetid is different for each asset. unsafeWindow.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 ? '取消所选物品' : '选中全部物品'); }); } // Sort the market listings. function sortMarketListings(elem, isPrice, isDate, isName) { var list = getListFromContainer(elem); if (list == null) { console.log('无效参数,找不到匹配元素的列表。'); 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 = DateTime.local().month; list.sort('market_listing_listed_date', { order: asc ? "asc" : "desc", sortFunction: function(a, b) { var firstDate = DateTime.fromString((a.values().market_listing_listed_date).trim(), 'd MMM'); var secondDate = DateTime.fromString((b.values().market_listing_listed_date).trim(), 'd MMM'); if (firstDate == null || secondDate == null) { return 0; } if (firstDate.month > currentMonth) firstDate = firstDate.plus({ years: -1}); if (secondDate.month > currentMonth) secondDate = secondDate.plus({ years: -1}); if (firstDate > secondDate) return 1; if (firstDate === secondDate) return 0; return -1; } }) } 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">选中全部物品</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">下架选中物品</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">重新上架选中物品</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">重新上架高价物品</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">选中高价物品</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">选中全部物品</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">删除选中物品</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.matchingItems.length; i++) { $('.market_select_item', marketList.matchingItems[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.matchingItems.length; i++) { if ($(marketList.matchingItems[i].elm).hasClass('overpriced')) { $('.market_select_item', marketList.matchingItems[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.matchingItems.length; i++) { if ($('.market_select_item', $(marketList.matchingItems[i].elm)).prop('checked')) { var listingid = replaceNonNumbers(marketList.matchingItems[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.matchingItems.length; i++) { if ($(marketList.matchingItems[i].elm).hasClass('overpriced')) { var listingid = replaceNonNumbers(marketList.matchingItems[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.matchingItems.length; i++) { if ($(marketList.matchingItems[i].elm).hasClass('overpriced') && $('.market_select_item', $(marketList.matchingItems[i].elm)).prop('checked')) { var listingid = replaceNonNumbers(marketList.matchingItems[i].values().market_listing_item_name); queueOverpricedItemListing(listingid); } } }); $('#see_settings').remove(); $('#global_action_menu').prepend('<span id="see_settings"><a href="javascript:void(0)">⬖ SEE 设置</a></span>'); $('#see_settings').on('click', '*', () => openSettings()); processMarketListings(); } } //#endregion //#region Tradeoffers if (currentPage == PAGE_TRADEOFFER) { // Gets the trade offer's inventory items from the active inventory. function getTradeOfferInventoryItems() { var arr = []; for (var child in getActiveInventory().rgChildInventories) { for (var key in getActiveInventory().rgChildInventories[child].rgInventory) { var value = getActiveInventory().rgChildInventories[child].rgInventory[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().rgInventory) { var value = getActiveInventory().rgInventory[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; } 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>物品数量:' + sortable.length + ',价值 ' + (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; } } var lastTradeOfferSum = 0; function hasLoadedAllTradeOfferItems() { for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; i++) { var asset = UserThem.findAsset(unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].assetid); if (asset == null) return false; } for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.me.assets.length; i++) { var asset = UserYou.findAsset(unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].assetid); if (asset == null) return false; } return true; } function initializeTradeOfferUI() { var updateInventoryPrices = function() { if (getSettingWithDefault(SETTING_TRADEOFFER_PRICE_LABELS) == 1) { setInventoryPrices(getTradeOfferInventoryItems()); } }; var updateInventoryPricesInTrade = function() { var items = []; for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; i++) { var asset = UserThem.findAsset(unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].assetid); items.push(asset); } for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.me.assets.length; i++) { var asset = UserYou.findAsset(unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].assetid); items.push(asset); } setInventoryPrices(items); }; $('.trade_right > div > div > div > .trade_item_box').observe('childlist subtree', function(record) { if (!hasLoadedAllTradeOfferItems()) return; var currentTradeOfferSum = unsafeWindow.g_rgCurrentTradeStatus.me.assets.length + unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; if (lastTradeOfferSum != currentTradeOfferSum) { updateInventoryPricesInTrade(); } lastTradeOfferSum = currentTradeOfferSum; $('#trade_offer_your_sum').remove(); $('#trade_offer_their_sum').remove(); var your_sum = sumTradeOfferAssets(unsafeWindow.g_rgCurrentTradeStatus.me.assets, UserYou); var their_sum = sumTradeOfferAssets(unsafeWindow.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>'); }); // Load after the inventory is loaded. updateInventoryPrices(); $('#inventory_pagecontrols').observe('childlist', '*', function(record) { updateInventoryPrices(); }); // This only works with a new trade offer. if (!window.location.href.includes('tradeoffer/new')) return; $('#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">选中页面中全部物品</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; unsafeWindow.MoveItemToTrade(it); }); }); } //#endregion //#region Settings function openSettings() { var price_options = $('<div id="price_options">' + '<div style="margin-bottom:6px;">' + '基准价格计算方式:<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"' : '') + '>历史均价 和 最低售价 之间的最大值</option>' + '<option value="2" ' + (getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 2 ? 'selected="selected"' : '') + '>最低售价</option>' + '<option value="3" ' + (getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3 ? 'selected="selected"' : '') + '>当前 最高买入价 或 最低售价</option>' + '</select>' + '<br/>' + '</div>' + '<div style="margin-bottom:6px;">' + '计算多少小时内的历史均价:<input class="price_option_input" style="background-color: black;color: white;border: transparent;" type="number" step="2" id="' + SETTING_PRICE_HISTORY_HOURS + '" value=' + getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) + '>' + '</div>' + '<div style="margin-bottom:6px;">' + '价格补正(基于“基准价格”在批量出售时进行调价,可为负数);<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">' + '在当前最低售价较少时,使用第二低售价:<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:6px;">' + '不检查指定价格及以下的市场列表:<input class="price_option_input price_option_price" style="background-color: black;color: white;border: transparent;" type="number" step="0.01" id="' + SETTING_PRICE_MIN_CHECK_PRICE + '" value=' + getSettingWithDefault(SETTING_PRICE_MIN_CHECK_PRICE) + '>' + '<br/>' + '</div>' + '<div style="margin-top:24px">' + '在库存中显示价格标签:<input class="price_option_input" style="background-color: black;color: white;border: transparent;" type="checkbox" id="' + SETTING_INVENTORY_PRICE_LABELS + '" ' + (getSettingWithDefault(SETTING_INVENTORY_PRICE_LABELS) == 1 ? 'checked=""' : '') + '>' + '</div>' + '<div style="margin-top:6px">' + '在交易报价中显示价格标签:<input class="price_option_input" style="background-color: black;color: white;border: transparent;" type="checkbox" id="' + SETTING_TRADEOFFER_PRICE_LABELS + '" ' + (getSettingWithDefault(SETTING_TRADEOFFER_PRICE_LABELS) == 1 ? 'checked=""' : '') + '>' + '</div>' + '<div style="margin-top:24px">' + '<div style="margin-bottom:6px;">' + '最低售价:<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) + '> ' + '最高售价:<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) + '> 普通卡牌的价格' + '<br/>' + '</div>' + '<div style="margin-bottom:6px;">' + '最低售价:<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) + '> ' + '最高售价:<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) + '> 闪亮卡牌的价格' + '<br/>' + '</div>' + '<div style="margin-bottom:6px;">' + '最低售价:<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) + '> ' + '最高售价:<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) + '> 其他物品的价格' + '<br/>' + '</div>' + '<div style="margin-top:24px;margin-bottom:6px;">' + '市场中每页的物品数量:<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;">' + '自动上架定价高于市场的物品(当库存较多时,耗时较高):<input id="' + SETTING_RELIST_AUTOMATICALLY + '" class="market_relist_auto" type="checkbox" ' + (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1 ? 'checked=""' : '') + '>' + '</label>' + '</div>' + '</div>' + '</div>'); var dialog = unsafeWindow.ShowConfirmDialog('Steam Economy Enhancer', 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_MIN_CHECK_PRICE, $('#' + SETTING_PRICE_MIN_CHECK_PRICE, price_options).val()); setSetting(SETTING_PRICE_ALGORITHM, $('#' + SETTING_PRICE_ALGORITHM, price_options).val()); setSetting(SETTING_PRICE_IGNORE_LOWEST_Q, $('#' + SETTING_PRICE_IGNORE_LOWEST_Q, price_options).prop('checked') ? 1 : 0); setSetting(SETTING_PRICE_HISTORY_HOURS, $('#' + SETTING_PRICE_HISTORY_HOURS, 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_INVENTORY_PRICE_LABELS, $('#' + SETTING_INVENTORY_PRICE_LABELS, price_options).prop('checked') ? 1 : 0); setSetting(SETTING_TRADEOFFER_PRICE_LABELS, $('#' + SETTING_TRADEOFFER_PRICE_LABELS, price_options).prop('checked') ? 1 : 0); window.location.reload(); }); } //#endregion //#region UI injectCss('.ui-selected { outline: 2px dashed #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: 24px; line-height:24px; 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;}' + '.separator-btn-right {margin-right:12px;}' + '.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);