├── LICENSE ├── README.md └── code.user.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam Economy Enhancer 2 | 3 | A free userscript to enhance your Steam Inventory, Steam Market and Steam Tradeoffers. 4 | 5 | It adds the following features to the Steam Market: 6 | 7 | * Detect overpriced and underpriced items. 8 | * Select 5/25/all (overpriced) items and remove them at once. 9 | * (Automatically) relist overpriced items. 10 | * Sort and search items by name, price or date. 11 | * Total price for listings, as seller and buyer. 12 | 13 | It adds the following features to the Steam Inventory: 14 | 15 | * Sell all (selected) items or trading cards automatically. 16 | * Select multiple items simultaneously with *Shift* or *Ctrl*. 17 | * Market sell and buy listings added to the item details. 18 | * Quick sell buttons to sell an item without confirmations. 19 | * Shows the lowest listed price for each item. 20 | * Turn selected items into gems. 21 | * Unpack selected booster packs. 22 | 23 | It adds the following features to the Steam Tradeoffers: 24 | 25 | * A summary of all items from both parties that includes total number of items, number of unique items and item count breakdown (how many of each item there are) 26 | * Select all items of the current page. 27 | * Shows the lowest listed price for each inventory item. 28 | 29 | The pricing can be based on the lowest listed price, the price history and your own minimum and maximum prices. 30 | This can be defined in Steam Economy Enhancer's settings, which you can find at the top of the page near the *Install Steam* button. 31 | 32 | > [!NOTE] 33 | > It is free but there is **NO** support. If you want to add functionality, feel free to submit a PR. 34 | 35 | ### Download 36 | 37 | [Install Steam Economy Enhancer](https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js) 38 | 39 | *[Violentmonkey](https://violentmonkey.github.io/) is required to install.* 40 | 41 | ### Screenshots 42 | 43 | 44 | *Market* 45 | 46 | ![Market](https://github.com/Nuklon/Steam-Economy-Enhancer/raw/refs/heads/master/assets/market.png) 47 | 48 | 49 | *Inventory* 50 | 51 | ![Inventory](https://github.com/Nuklon/Steam-Economy-Enhancer/raw/refs/heads/master/assets/inventory.png) 52 | 53 | 54 | *Options* 55 | 56 | ![Options](https://github.com/Nuklon/Steam-Economy-Enhancer/raw/refs/heads/master/assets/settings.png) 57 | 58 | 59 | *Trade offers* 60 | 61 | ![Tradeoffers](https://github.com/Nuklon/Steam-Economy-Enhancer/raw/refs/heads/master/assets/tradeoffer.png) 62 | 63 | 64 | ### License 65 | 66 | [MIT](LICENSE) 67 | -------------------------------------------------------------------------------- /code.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Steam Economy Enhancer 3 | // @icon data:image/svg+xml,%0A%3Csvg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 267 267"%3E%3Ccircle cx="133.3" cy="133.3" r="133.3" fill="%2326566c"/%3E%3Cpath fill="%23ebebeb" fill-rule="nonzero" d="m50 133 83-83 84 83-84 84-83-84Zm83 62 62-61-62-62v123Z"/%3E%3C/svg%3E 4 | // @namespace https://github.com/Nuklon 5 | // @author Nuklon 6 | // @license MIT 7 | // @version 7.1.12 8 | // @description 增强 Steam 库存和 Steam 市场功能 9 | // @match *://steamcommunity.com/id/*/inventory* 10 | // @match *://steamcommunity.com/profiles/*/inventory* 11 | // @match *://steamcommunity.com/market* 12 | // @match *://steamcommunity.com/tradeoffer* 13 | // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js 14 | // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/jquery-ui.min.js 15 | // @require https://cdnjs.cloudflare.com/ajax/libs/async/3.2.6/async.js 16 | // @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js 17 | // @require https://cdnjs.cloudflare.com/ajax/libs/luxon/3.5.0/luxon.min.js 18 | // @require https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.js 19 | // @require https://raw.githubusercontent.com/kapetan/jquery-observe/ca67b735bb3ae8d678d1843384ebbe7c02466c61/jquery-observe.js 20 | // @require https://raw.githubusercontent.com/rmariuzzo/checkboxes.js/91bec667e9172ceb063df1ecb7505e8ed0bae9ba/src/jquery.checkboxes.js 21 | // @grant unsafeWindow 22 | // @homepageURL https://keylol.com/t311996-1-1 23 | // @homepage https://keylol.com/t311996-1-1 24 | // @supportURL https://keylol.com/t311996-1-1 25 | // @downloadURL https://raw.githubusercontent.com/Sneer-Cat/Steam-Economy-Enhancer/master/code.user.js 26 | // @updateURL https://raw.githubusercontent.com/Sneer-Cat/Steam-Economy-Enhancer/master/code.user.js 27 | // ==/UserScript== 28 | 29 | /* disable some eslint rules until the code is cleaned up */ 30 | /* global unsafeWindow, luxon, jQuery, async, List, localforage */ 31 | /* eslint no-undef: off */ 32 | 33 | // jQuery is already added by Steam, force no conflict mode. 34 | (function ($, async) { 35 | $.noConflict(true); 36 | 37 | const PAGE_MARKET = 0; 38 | const PAGE_MARKET_LISTING = 1; 39 | const PAGE_TRADEOFFER = 2; 40 | const PAGE_INVENTORY = 3; 41 | 42 | const COLOR_ERROR = '#8A4243'; 43 | const COLOR_SUCCESS = '#407736'; 44 | const COLOR_PENDING = '#908F44'; 45 | const COLOR_PRICE_FAIR = '#496424'; 46 | const COLOR_PRICE_CHEAP = '#837433'; 47 | const COLOR_PRICE_EXPENSIVE = '#813030'; 48 | const COLOR_PRICE_NOT_CHECKED = '#26566c'; 49 | 50 | const ERROR_SUCCESS = null; 51 | const ERROR_FAILED = 1; 52 | const ERROR_DATA = 2; 53 | 54 | const marketLists = []; 55 | let totalNumberOfProcessedQueueItems = 0; 56 | let totalNumberOfQueuedItems = 0; 57 | let totalPriceWithFeesOnMarket = 0; 58 | let totalPriceWithoutFeesOnMarket = 0; 59 | let totalScrap = 0; 60 | 61 | const spinnerBlock = 62 | '
 
 
 
 
 
'; 63 | let numberOfFailedRequests = 0; 64 | 65 | const enableConsoleLog = false; 66 | 67 | const country = typeof unsafeWindow.g_strCountryCode !== 'undefined' ? unsafeWindow.g_strCountryCode : undefined; 68 | const isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.g_bLoggedIn; 69 | 70 | const currentPage = window.location.href.includes('.com/market') 71 | ? window.location.href.includes('market/listings') 72 | ? PAGE_MARKET_LISTING 73 | : PAGE_MARKET 74 | : window.location.href.includes('.com/tradeoffer') 75 | ? PAGE_TRADEOFFER 76 | : PAGE_INVENTORY; 77 | 78 | const market = new SteamMarket( 79 | unsafeWindow.g_rgAppContextData, 80 | getInventoryUrl(), 81 | isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined 82 | ); 83 | 84 | const currencyId = 85 | isLoggedIn && 86 | market != null && 87 | market.walletInfo != null && 88 | market.walletInfo.wallet_currency != null 89 | ? market.walletInfo.wallet_currency 90 | : 3; 91 | 92 | const currencyCountry = 93 | isLoggedIn && 94 | market != null && 95 | market.walletInfo != null && 96 | market.walletInfo.wallet_country != null 97 | ? market.walletInfo.wallet_country 98 | : 'US'; 99 | 100 | const currencyCode = unsafeWindow.GetCurrencyCode(currencyId); 101 | 102 | function SteamMarket(appContext, inventoryUrl, walletInfo) { 103 | this.appContext = appContext; 104 | this.inventoryUrl = inventoryUrl; 105 | this.walletInfo = walletInfo; 106 | this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', ''); 107 | if (!this.inventoryUrlBase.endsWith('/')) { 108 | this.inventoryUrlBase += '/'; 109 | } 110 | } 111 | 112 | function request(url, options, callback) { 113 | let delayBetweenRequests = 300; 114 | let requestStorageHash = 'see:request:last'; 115 | 116 | if (url.startsWith('https://steamcommunity.com/market/')) { 117 | requestStorageHash = `${requestStorageHash}:steamcommunity.com/market`; 118 | delayBetweenRequests = 1000; 119 | } 120 | 121 | const lastRequest = JSON.parse(getLocalStorageItem(requestStorageHash) || JSON.stringify({ time: new Date(0), limited: false })); 122 | const timeSinceLastRequest = Date.now() - new Date(lastRequest.time).getTime(); 123 | 124 | delayBetweenRequests = lastRequest.limited ? 2.5 * 60 * 1000 : delayBetweenRequests; 125 | 126 | if (timeSinceLastRequest < delayBetweenRequests) { 127 | setTimeout(() => request(...arguments), delayBetweenRequests - timeSinceLastRequest); 128 | return; 129 | } 130 | 131 | lastRequest.time = new Date(); 132 | lastRequest.limited = false; 133 | 134 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest)); 135 | 136 | $.ajax({ 137 | url: url, 138 | type: options.method, 139 | data: options.data, 140 | success: function (data, statusMessage, xhr) { 141 | if (xhr.status === 429) { 142 | lastRequest.limited = true; 143 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest)); 144 | } 145 | 146 | if (xhr.status >= 400) { 147 | const error = new Error('HTTP 错误'); 148 | error.statusCode = xhr.status; 149 | 150 | callback(error, data); 151 | } else { 152 | callback(null, data) 153 | } 154 | }, 155 | error: (xhr) => { 156 | if (xhr.status === 429) { 157 | lastRequest.limited = true; 158 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest)); 159 | } 160 | 161 | const error = new Error('请求失败'); 162 | error.statusCode = xhr.status; 163 | 164 | callback(error); 165 | }, 166 | dataType: options.responseType 167 | }); 168 | }; 169 | 170 | function getInventoryUrl() { 171 | if (unsafeWindow.g_strInventoryLoadURL) { 172 | return unsafeWindow.g_strInventoryLoadURL; 173 | } 174 | 175 | let profileUrl = `${window.location.origin}/my/`; 176 | 177 | if (unsafeWindow.g_strProfileURL) { 178 | profileUrl = unsafeWindow.g_strProfileURL; 179 | } else { 180 | const avatar = document.querySelector('#global_actions a.user_avatar'); 181 | 182 | if (avatar) { 183 | profileUrl = avatar.href; 184 | } 185 | } 186 | 187 | return `${profileUrl.replace(/\/$/, '')}/inventory/json/`; 188 | } 189 | 190 | //#region Settings 191 | const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE'; 192 | const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE'; 193 | const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE'; 194 | const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE'; 195 | const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE'; 196 | const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE'; 197 | const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET'; 198 | const SETTING_PRICE_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE'; 199 | const SETTING_PRICE_MIN_LIST_PRICE = 'SETTING_PRICE_MIN_LIST_PRICE'; 200 | const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM'; 201 | const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q'; 202 | const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS'; 203 | const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS'; 204 | const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS'; 205 | const SETTING_QUICK_SELL_BUTTONS = 'SETTING_QUICK_SELL_BUTTONS'; 206 | const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE'; 207 | const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY'; 208 | 209 | const settingDefaults = { 210 | SETTING_MIN_NORMAL_PRICE: 0.05, 211 | SETTING_MAX_NORMAL_PRICE: 2.50, 212 | SETTING_MIN_FOIL_PRICE: 0.15, 213 | SETTING_MAX_FOIL_PRICE: 10, 214 | SETTING_MIN_MISC_PRICE: 0.05, 215 | SETTING_MAX_MISC_PRICE: 10, 216 | SETTING_PRICE_OFFSET: 0.00, 217 | SETTING_PRICE_MIN_CHECK_PRICE: 0.00, 218 | SETTING_PRICE_MIN_LIST_PRICE: 0.03, 219 | SETTING_PRICE_ALGORITHM: 1, 220 | SETTING_PRICE_IGNORE_LOWEST_Q: 1, 221 | SETTING_PRICE_HISTORY_HOURS: 12, 222 | SETTING_INVENTORY_PRICE_LABELS: 1, 223 | SETTING_TRADEOFFER_PRICE_LABELS: 1, 224 | SETTING_QUICK_SELL_BUTTONS: 1, 225 | SETTING_LAST_CACHE: 0, 226 | SETTING_RELIST_AUTOMATICALLY: 0 227 | }; 228 | 229 | function getSettingWithDefault(name) { 230 | return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null); 231 | } 232 | 233 | function setSetting(name, value) { 234 | setLocalStorageItem(name, value); 235 | } 236 | //#endregion 237 | 238 | //#region Storage 239 | 240 | const storagePersistent = localforage.createInstance({ 241 | name: 'see_persistent' 242 | }); 243 | 244 | let storageSession; 245 | 246 | const currentUrl = new URL(window.location.href); 247 | const noCache = currentUrl.searchParams.get('no-cache') != null; 248 | 249 | // This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache. 250 | // For this reason, a rolling cache is used. 251 | if (getSessionStorageItem('SESSION') == null || noCache) { 252 | let lastCache = getSettingWithDefault(SETTING_LAST_CACHE); 253 | if (lastCache > 5) { 254 | lastCache = 0; 255 | } 256 | 257 | setSetting(SETTING_LAST_CACHE, lastCache + 1); 258 | 259 | storageSession = localforage.createInstance({ 260 | name: `see_session_${lastCache}` 261 | }); 262 | 263 | storageSession.clear(); // Clear any previous data. 264 | setSessionStorageItem('SESSION', lastCache); 265 | } else { 266 | storageSession = localforage.createInstance({ 267 | name: `see_session_${getSessionStorageItem('SESSION')}` 268 | }); 269 | } 270 | 271 | function getLocalStorageItem(name) { 272 | try { 273 | return localStorage.getItem(name); 274 | } catch (e) { 275 | logConsole(`无法获取 localStorage 内容,名称:${name},原因:${e}。`); 276 | return null; 277 | } 278 | } 279 | 280 | function setLocalStorageItem(name, value) { 281 | try { 282 | localStorage.setItem(name, value); 283 | return true; 284 | } catch (e) { 285 | logConsole(`无法设置 localStorage 内容,名称:${name},原因:${e}。`) 286 | return false; 287 | } 288 | } 289 | 290 | function getSessionStorageItem(name) { 291 | try { 292 | return sessionStorage.getItem(name); 293 | } catch (e) { 294 | logConsole(`无法获取 sessionStorage 内容,名称:${name},原因:${e}。`); 295 | return null; 296 | } 297 | } 298 | 299 | function setSessionStorageItem(name, value) { 300 | try { 301 | sessionStorage.setItem(name, value); 302 | return true; 303 | } catch (e) { 304 | logConsole(`无法设置 sessionStorage 内容,名称:${name},原因:${e}。`) 305 | return false; 306 | } 307 | } 308 | //#endregion 309 | 310 | //#region Price helpers 311 | function formatPrice(valueInCents) { 312 | return unsafeWindow.v_currencyformat(valueInCents, currencyCode, currencyCountry); 313 | } 314 | 315 | function getPriceInformationFromItem(item) { 316 | const isTradingCard = getIsTradingCard(item); 317 | const isFoilTradingCard = getIsFoilTradingCard(item); 318 | return getPriceInformation(isTradingCard, isFoilTradingCard); 319 | } 320 | 321 | function getPriceInformation(isTradingCard, isFoilTradingCard) { 322 | let maxPrice = 0; 323 | let minPrice = 0; 324 | 325 | if (!isTradingCard) { 326 | maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE); 327 | minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE); 328 | } else { 329 | maxPrice = isFoilTradingCard 330 | ? getSettingWithDefault(SETTING_MAX_FOIL_PRICE) 331 | : getSettingWithDefault(SETTING_MAX_NORMAL_PRICE); 332 | minPrice = isFoilTradingCard 333 | ? getSettingWithDefault(SETTING_MIN_FOIL_PRICE) 334 | : getSettingWithDefault(SETTING_MIN_NORMAL_PRICE); 335 | } 336 | 337 | maxPrice = maxPrice * 100.0; 338 | minPrice = minPrice * 100.0; 339 | 340 | const maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice); 341 | const minPriceBeforeFees = market.getPriceBeforeFees(minPrice); 342 | 343 | return { 344 | maxPrice, 345 | minPrice, 346 | maxPriceBeforeFees, 347 | minPriceBeforeFees 348 | }; 349 | } 350 | 351 | // Calculates the average history price, before the fee. 352 | function calculateAverageHistoryPriceBeforeFees(history) { 353 | let highest = 0; 354 | let total = 0; 355 | 356 | if (history != null) { 357 | // Highest average price in the last xx hours. 358 | const timeAgo = Date.now() - getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 60 * 60 * 1000; 359 | 360 | history.forEach((historyItem) => { 361 | const d = new Date(historyItem[0]); 362 | if (d.getTime() > timeAgo) { 363 | highest += historyItem[1] * historyItem[2]; 364 | total += historyItem[2]; 365 | } 366 | }); 367 | } 368 | 369 | if (total == 0) { 370 | return 0; 371 | } 372 | 373 | highest = Math.floor(highest / total); 374 | return market.getPriceBeforeFees(highest); 375 | } 376 | 377 | // Calculates the listing price, before the fee. 378 | function calculateListingPriceBeforeFees(histogram) { 379 | if (typeof histogram === 'undefined' || 380 | histogram == null || 381 | histogram.lowest_sell_order == null || 382 | histogram.sell_order_graph == null) { 383 | return 0; 384 | } 385 | 386 | let listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order); 387 | 388 | const shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1; 389 | 390 | if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) { 391 | const listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100); 392 | 393 | if (listingPrice2ndLowest > listingPrice) { 394 | const numberOfListingsLowest = histogram.sell_order_graph[0][1]; 395 | const numberOfListings2ndLowest = histogram.sell_order_graph[1][1]; 396 | 397 | const percentageLower = 100 * (numberOfListingsLowest / numberOfListings2ndLowest); 398 | 399 | // The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25). 400 | if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) { 401 | listingPrice = listingPrice2ndLowest; 402 | } else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) { 403 | listingPrice = listingPrice2ndLowest; 404 | } else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) { 405 | listingPrice = listingPrice2ndLowest; 406 | } else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) { 407 | listingPrice = listingPrice2ndLowest; 408 | } else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) { 409 | listingPrice = listingPrice2ndLowest; 410 | } else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) { 411 | listingPrice = listingPrice2ndLowest; 412 | } 413 | } 414 | } 415 | 416 | return listingPrice; 417 | } 418 | 419 | function calculateBuyOrderPriceBeforeFees(histogram) { 420 | if (typeof histogram === 'undefined') { 421 | return 0; 422 | } 423 | 424 | return market.getPriceBeforeFees(histogram.highest_buy_order); 425 | } 426 | 427 | // Calculate the sell price based on the history and listings. 428 | // applyOffset specifies whether the price offset should be applied when the listings are used to determine the price. 429 | function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) { 430 | const historyPrice = calculateAverageHistoryPriceBeforeFees(history); 431 | const listingPrice = calculateListingPriceBeforeFees(histogram); 432 | const buyPrice = calculateBuyOrderPriceBeforeFees(histogram); 433 | 434 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1; 435 | const shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3; 436 | 437 | // If the highest average price is lower than the first listing, return the offset + that listing. 438 | // Otherwise, use the highest average price instead. 439 | let calculatedPrice = 0; 440 | if (shouldUseBuyOrder && buyPrice !== -2) { 441 | calculatedPrice = buyPrice; 442 | } else if (historyPrice < listingPrice || !shouldUseAverage) { 443 | calculatedPrice = listingPrice; 444 | } else { 445 | calculatedPrice = historyPrice; 446 | } 447 | 448 | let changedToMax = false; 449 | // List for the maximum price if there are no listings yet. 450 | if (calculatedPrice == 0) { 451 | calculatedPrice = maxPriceBeforeFees; 452 | changedToMax = true; 453 | } 454 | 455 | 456 | // 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). 457 | if (!changedToMax && applyOffset) { 458 | calculatedPrice = calculatedPrice + getSettingWithDefault(SETTING_PRICE_OFFSET) * 100; 459 | } 460 | 461 | 462 | // Keep our minimum and maximum in mind. 463 | calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees); 464 | 465 | 466 | // In case there's a buy order higher than the calculated price. 467 | if (typeof histogram !== 'undefined' && histogram != null && histogram.highest_buy_order != null) { 468 | const buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order); 469 | if (buyOrderPrice > calculatedPrice) { 470 | calculatedPrice = buyOrderPrice; 471 | } 472 | } 473 | 474 | return calculatedPrice; 475 | } 476 | //#endregion 477 | 478 | //#region Integer helpers 479 | function getRandomInt(min, max) { 480 | return Math.floor(Math.random() * (max - min + 1)) + min; 481 | } 482 | 483 | function getNumberOfDigits(x) { 484 | return (Math.log10((x ^ x >> 31) - (x >> 31)) | 0) + 1; 485 | } 486 | 487 | function padLeftZero(str, max) { 488 | str = str.toString(); 489 | return str.length < max ? padLeftZero(`0${str}`, max) : str; 490 | } 491 | 492 | function replaceNonNumbers(str) { 493 | return str.replace(/\D/g, ''); 494 | } 495 | //#endregion 496 | 497 | //#region Steam Market 498 | 499 | // Sell an item with a price in cents. 500 | // Price is before fees. 501 | SteamMarket.prototype.sellItem = function (item, price, callback /*err, data*/) { 502 | const url = `${window.location.origin}/market/sellitem/`; 503 | 504 | const options = { 505 | method: 'POST', 506 | data: { 507 | sessionid: readCookie('sessionid'), 508 | appid: item.appid, 509 | contextid: item.contextid, 510 | assetid: item.assetid || item.id, 511 | amount: item.amount || 1, 512 | price: price 513 | }, 514 | responseType: 'json' 515 | }; 516 | 517 | request(url, options, callback); 518 | }; 519 | 520 | // Removes an item. 521 | // Item is the unique item id. 522 | SteamMarket.prototype.removeListing = function (item, isBuyOrder, callback /*err, data*/) { 523 | const url = isBuyOrder 524 | ? `${window.location.origin}/market/cancelbuyorder/` 525 | : `${window.location.origin}/market/removelisting/${item}`; 526 | 527 | const options = { 528 | method: 'POST', 529 | data: { 530 | sessionid: readCookie('sessionid'), 531 | ...(isBuyOrder ? { buy_orderid: item } : {}) 532 | }, 533 | responseType: 'json' 534 | }; 535 | 536 | request( 537 | url, 538 | options, 539 | (error, data) => { 540 | if (error) { 541 | callback(ERROR_FAILED); 542 | return; 543 | } 544 | 545 | callback(ERROR_SUCCESS, data); 546 | } 547 | ); 548 | }; 549 | 550 | // Get the price history for an item. 551 | // 552 | // PriceHistory is an array of prices in the form [data, price, number sold]. 553 | // Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]] 554 | // Prices are ordered by oldest to most recent. 555 | // Price is inclusive of fees. 556 | SteamMarket.prototype.getPriceHistory = function (item, cache, callback) { 557 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1; 558 | 559 | if (!shouldUseAverage) { 560 | // The price history is only used by the "average price" calculation 561 | return callback(ERROR_SUCCESS, null, true); 562 | } 563 | 564 | try { 565 | const market_name = getMarketHashName(item); 566 | if (market_name == null) { 567 | callback(ERROR_FAILED); 568 | return; 569 | } 570 | 571 | const appid = item.appid; 572 | 573 | if (cache) { 574 | const storage_hash = `pricehistory_${appid}+${market_name}`; 575 | 576 | storageSession.getItem(storage_hash). 577 | then((value) => { 578 | if (value != null) { 579 | callback(ERROR_SUCCESS, value, true); 580 | } else { 581 | market.getCurrentPriceHistory(appid, market_name, callback); 582 | } 583 | }). 584 | catch(() => { 585 | market.getCurrentPriceHistory(appid, market_name, callback); 586 | }); 587 | } else { 588 | market.getCurrentPriceHistory(appid, market_name, callback); 589 | } 590 | } catch { 591 | return callback(ERROR_FAILED); 592 | } 593 | }; 594 | 595 | SteamMarket.prototype.getGooValue = function (item, callback) { 596 | try { 597 | let appid = item.market_fee_app; 598 | 599 | for (const action of item.owner_actions) { 600 | if (!action.link || !action.link.startsWith('javascript:GetGooValue')) { 601 | continue; 602 | } 603 | 604 | let item_data = action.link.split(','); 605 | let appid = item_data[2].trim(); 606 | let item_type = item_data[3].trim(); 607 | let border_color = item_data[4].split(' ')[0].trim(); 608 | 609 | const url = `${window.location.origin}/auction/ajaxgetgoovalueforitemtype`; 610 | 611 | const options = { 612 | method: 'GET', 613 | data: { 614 | appid: appid, 615 | item_type: item_type, 616 | border_color: border_color 617 | }, 618 | responseType: 'json' 619 | }; 620 | 621 | request( 622 | url, 623 | options, 624 | (error, data) => { 625 | if (error) { 626 | callback(ERROR_FAILED, data); 627 | return; 628 | } 629 | 630 | callback(ERROR_SUCCESS, data); 631 | } 632 | ); 633 | } 634 | } catch (e) { 635 | return callback(ERROR_FAILED); 636 | } 637 | //http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0 638 | // OR 639 | //http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6 640 | //sessionid=xyz 641 | //appid = 535690 642 | //assetid = 4830605461 643 | //contextid = 6 644 | }; 645 | 646 | 647 | // Grinds the item into gems. 648 | SteamMarket.prototype.grindIntoGoo = function (item, callback) { 649 | try { 650 | const url = `${this.inventoryUrlBase}ajaxgrindintogoo/`; 651 | 652 | const options = { 653 | method: 'POST', 654 | data: { 655 | sessionid: readCookie('sessionid'), 656 | appid: item.market_fee_app, 657 | assetid: item.assetid, 658 | contextid: item.contextid, 659 | goo_value_expected: item.goo_value_expected 660 | }, 661 | responseType: 'json' 662 | }; 663 | 664 | request( 665 | url, 666 | options, 667 | (error, data) => { 668 | if (error) { 669 | callback(ERROR_FAILED, data); 670 | return; 671 | } 672 | 673 | callback(ERROR_SUCCESS, data); 674 | } 675 | ); 676 | } catch { 677 | return callback(ERROR_FAILED); 678 | } 679 | 680 | //sessionid = xyz 681 | //appid = 535690 682 | //assetid = 4830605461 683 | //contextid = 6 684 | //goo_value_expected = 10 685 | //http://steamcommunity.com/my/ajaxgrindintogoo/ 686 | }; 687 | 688 | 689 | // Unpacks the booster pack. 690 | SteamMarket.prototype.unpackBoosterPack = function (item, callback) { 691 | try { 692 | const url = `${this.inventoryUrlBase}ajaxunpackbooster/`; 693 | 694 | const options = { 695 | method: 'POST', 696 | data: { 697 | sessionid: readCookie('sessionid'), 698 | appid: item.market_fee_app, 699 | communityitemid: item.assetid 700 | }, 701 | responseType: 'json' 702 | }; 703 | 704 | request( 705 | url, 706 | options, 707 | (error, data) => { 708 | if (error) { 709 | callback(ERROR_FAILED, data); 710 | return; 711 | } 712 | 713 | callback(ERROR_SUCCESS, data); 714 | } 715 | ); 716 | } catch { 717 | return callback(ERROR_FAILED); 718 | } 719 | 720 | //sessionid = xyz 721 | //appid = 535690 722 | //communityitemid = 4830605461 723 | //http://steamcommunity.com/my/ajaxunpackbooster/ 724 | }; 725 | 726 | // Get the current price history for an item. 727 | SteamMarket.prototype.getCurrentPriceHistory = function (appid, market_name, callback) { 728 | const url = `${window.location.origin}/market/pricehistory/`; 729 | 730 | const options = { 731 | method: 'GET', 732 | data: { 733 | appid: appid, 734 | market_hash_name: market_name 735 | }, 736 | responseType: 'json' 737 | }; 738 | 739 | request( 740 | url, 741 | options, 742 | (error, data) => { 743 | if (error) { 744 | callback(ERROR_FAILED); 745 | return; 746 | } 747 | 748 | if (data && (!data.success || !data.prices)) { 749 | callback(ERROR_DATA); 750 | return; 751 | } 752 | 753 | // Multiply prices so they're in pennies. 754 | for (let i = 0; i < data.prices.length; i++) { 755 | data.prices[i][1] *= 100; 756 | data.prices[i][2] = parseInt(data.prices[i][2]); 757 | } 758 | 759 | // Store the price history in the session storage. 760 | const storage_hash = `pricehistory_${appid}+${market_name}`; 761 | storageSession.setItem(storage_hash, data.prices); 762 | 763 | callback(ERROR_SUCCESS, data.prices, false); 764 | } 765 | ); 766 | }; 767 | 768 | // Get the item name id from a market item. 769 | // 770 | // This id never changes so we can store this in the persistent storage. 771 | SteamMarket.prototype.getMarketItemNameId = function (item, callback) { 772 | try { 773 | const market_name = getMarketHashName(item); 774 | if (market_name == null) { 775 | callback(ERROR_FAILED); 776 | return; 777 | } 778 | 779 | const appid = item.appid; 780 | const storage_hash = `itemnameid_${appid}+${market_name}`; 781 | 782 | storagePersistent.getItem(storage_hash). 783 | then((value) => { 784 | if (value != null) { 785 | callback(ERROR_SUCCESS, value); 786 | } else { 787 | return market.getCurrentMarketItemNameId(appid, market_name, callback); 788 | } 789 | }). 790 | catch(() => { 791 | return market.getCurrentMarketItemNameId(appid, market_name, callback); 792 | }); 793 | } catch { 794 | return callback(ERROR_FAILED); 795 | } 796 | }; 797 | 798 | // Get the item name id from a market item. 799 | SteamMarket.prototype.getCurrentMarketItemNameId = function (appid, market_name, callback) { 800 | const url = `${window.location.origin}/market/listings/${appid}/${escapeURI(market_name)}`; 801 | 802 | const options = { method: 'GET' }; 803 | 804 | request( 805 | url, 806 | options, 807 | (error, data) => { 808 | if (error) { 809 | callback(ERROR_FAILED); 810 | return; 811 | } 812 | 813 | const matches = (/Market_LoadOrderSpread\( (\d+) \);/).exec(data || ''); 814 | if (matches == null) { 815 | callback(ERROR_DATA); 816 | return; 817 | } 818 | 819 | const item_nameid = matches[1]; 820 | 821 | // Store the item name id in the persistent storage. 822 | const storage_hash = `itemnameid_${appid}+${market_name}`; 823 | storagePersistent.setItem(storage_hash, item_nameid); 824 | 825 | callback(ERROR_SUCCESS, item_nameid); 826 | } 827 | ); 828 | }; 829 | 830 | // Get the sales listings for this item in the market, with more information. 831 | // 832 | //{ 833 | //"success" : 1, 834 | //"sell_order_table" : "
Price<\/th>Quantity<\/th><\/tr>
0,04\u20ac<\/td>311<\/td><\/tr>
0,05\u20ac<\/td>895<\/td><\/tr>
0,06\u20ac<\/td>495<\/td><\/tr>
0,07\u20ac<\/td>174<\/td><\/tr>
0,08\u20ac<\/td>49<\/td><\/tr>
0,09\u20ac or more<\/td>41<\/td><\/tr><\/table>", 835 | //"sell_order_summary" : "1965<\/span> for sale starting at 0,04\u20ac<\/span>", 836 | //"buy_order_table" : "
Price<\/th>Quantity<\/th><\/tr>
0,03\u20ac<\/td>93<\/td><\/tr><\/table>", 837 | //"buy_order_summary" : "93<\/span> requests to buy at 0,03\u20ac<\/span> or lower", 838 | //"highest_buy_order" : "3", 839 | //"lowest_sell_order" : "4", 840 | //"buy_order_graph" : [[0.03, 93, "93 buy orders at 0,03\u20ac or higher"]], 841 | //"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"]], 842 | //"graph_max_y" : 3000, 843 | //"graph_min_x" : 0.03, 844 | //"graph_max_x" : 0.14, 845 | //"price_prefix" : "", 846 | //"price_suffix" : "\u20ac" 847 | //} 848 | SteamMarket.prototype.getItemOrdersHistogram = function (item, cache, callback) { 849 | try { 850 | const market_name = getMarketHashName(item); 851 | if (market_name == null) { 852 | callback(ERROR_FAILED); 853 | return; 854 | } 855 | 856 | const appid = item.appid; 857 | 858 | if (cache) { 859 | const storage_hash = `itemordershistogram_${appid}+${market_name}`; 860 | storageSession.getItem(storage_hash). 861 | then((value) => { 862 | if (value != null) { 863 | callback(ERROR_SUCCESS, value, true); 864 | } else { 865 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 866 | } 867 | }). 868 | catch(() => { 869 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 870 | }); 871 | } else { 872 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 873 | } 874 | 875 | } catch { 876 | return callback(ERROR_FAILED); 877 | } 878 | }; 879 | 880 | // Get the sales listings for this item in the market, with more information. 881 | SteamMarket.prototype.getCurrentItemOrdersHistogram = function (item, market_name, callback) { 882 | market.getMarketItemNameId( 883 | item, 884 | (error, item_nameid) => { 885 | if (error) { 886 | callback(ERROR_FAILED); 887 | return; 888 | } 889 | 890 | const url = `${window.location.origin}/market/itemordershistogram`; 891 | 892 | const options = { 893 | method: 'GET', 894 | data: { 895 | country: country, 896 | language: 'schinese', 897 | currency: currencyId, 898 | item_nameid: item_nameid, 899 | two_factor: 0 900 | } 901 | }; 902 | 903 | request( 904 | url, 905 | options, 906 | (error, data) => { 907 | if (error) { 908 | callback(ERROR_FAILED, null); 909 | return; 910 | } 911 | 912 | // Store the histogram in the session storage. 913 | const storage_hash = `itemordershistogram_${item.appid}+${market_name}`; 914 | storageSession.setItem(storage_hash, data); 915 | 916 | callback(ERROR_SUCCESS, data, false); 917 | } 918 | ) 919 | } 920 | ); 921 | }; 922 | 923 | // Calculate the price before fees (seller price) from the buyer price 924 | SteamMarket.prototype.getPriceBeforeFees = function (price, item) { 925 | let publisherFee = -1; 926 | 927 | if (item != null) { 928 | if (item.market_fee != null) { 929 | publisherFee = item.market_fee; 930 | } else if (item.description != null && item.description.market_fee != null) { 931 | publisherFee = item.description.market_fee; 932 | } 933 | } 934 | 935 | if (publisherFee == -1) { 936 | if (this.walletInfo != null) { 937 | publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; 938 | } else { 939 | publisherFee = 0.10; 940 | } 941 | } 942 | 943 | price = Math.round(price); 944 | const feeInfo = CalculateFeeAmount(price, publisherFee, this.walletInfo); 945 | return price - feeInfo.fees; 946 | }; 947 | 948 | // Calculate the buyer price from the seller price 949 | SteamMarket.prototype.getPriceIncludingFees = function (price, item) { 950 | let publisherFee = -1; 951 | if (item != null) { 952 | if (item.market_fee != null) { 953 | publisherFee = item.market_fee; 954 | } else if (item.description != null && item.description.market_fee != null) { 955 | publisherFee = item.description.market_fee; 956 | } 957 | } 958 | if (publisherFee == -1) { 959 | if (this.walletInfo != null) { 960 | publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; 961 | } else { 962 | publisherFee = 0.10; 963 | } 964 | } 965 | 966 | price = Math.round(price); 967 | const feeInfo = CalculateAmountToSendForDesiredReceivedAmount(price, publisherFee, this.walletInfo); 968 | return feeInfo.amount; 969 | }; 970 | //#endregion 971 | 972 | // Cannot use encodeURI / encodeURIComponent, Steam only escapes certain characters. 973 | function escapeURI(name) { 974 | let previousName = ''; 975 | while (previousName != name) { 976 | previousName = name; 977 | name = name.replace('?', '%3F'). 978 | replace('#', '%23'). 979 | replace(' ', '%09'); 980 | } 981 | return name; 982 | } 983 | 984 | //#region Steam Market / Inventory helpers 985 | function getMarketHashName(item) { 986 | if (item == null) { 987 | return null; 988 | } 989 | 990 | if (item.description != null && item.description.market_hash_name != null) { 991 | return item.description.market_hash_name; 992 | } 993 | 994 | if (item.description != null && item.description.name != null) { 995 | return item.description.name; 996 | } 997 | 998 | if (item.market_hash_name != null) { 999 | return item.market_hash_name; 1000 | } 1001 | 1002 | if (item.name != null) { 1003 | return item.name; 1004 | } 1005 | 1006 | return null; 1007 | } 1008 | 1009 | function getIsCrate(item) { 1010 | if (item == null) { 1011 | return false; 1012 | } 1013 | // This is available on the inventory page. 1014 | const tags = item.tags != null 1015 | ? item.tags 1016 | : item.description != null && item.description.tags != null 1017 | ? item.description.tags 1018 | : null; 1019 | if (tags != null) { 1020 | let isTaggedAsCrate = false; 1021 | tags.forEach((arrayItem) => { 1022 | if (arrayItem.category == 'Type') { 1023 | if (arrayItem.internal_name == 'Supply Crate') { 1024 | isTaggedAsCrate = true; 1025 | } 1026 | } 1027 | }); 1028 | if (isTaggedAsCrate) { 1029 | return true; 1030 | } 1031 | } 1032 | } 1033 | 1034 | function getIsTradingCard(item) { 1035 | if (item == null) { 1036 | return false; 1037 | } 1038 | 1039 | // This is available on the inventory page. 1040 | const tags = item.tags != null 1041 | ? item.tags 1042 | : item.description != null && item.description.tags != null 1043 | ? item.description.tags 1044 | : null; 1045 | if (tags != null) { 1046 | let isTaggedAsTradingCard = false; 1047 | tags.forEach((arrayItem) => { 1048 | if (arrayItem.category == 'item_class') { 1049 | if (arrayItem.internal_name == 'item_class_2') { // trading card. 1050 | isTaggedAsTradingCard = true; 1051 | } 1052 | } 1053 | }); 1054 | if (isTaggedAsTradingCard) { 1055 | return true; 1056 | } 1057 | } 1058 | 1059 | // This is available on the market page. 1060 | if (item.owner_actions != null) { 1061 | for (let i = 0; i < item.owner_actions.length; i++) { 1062 | if (item.owner_actions[i].link == null) { 1063 | continue; 1064 | } 1065 | 1066 | // Cards include a link to the gamecard page. 1067 | // For example: "http://steamcommunity.com/my/gamecards/503820/". 1068 | if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards')) { 1069 | return true; 1070 | } 1071 | } 1072 | } 1073 | 1074 | // A fallback for the market page (only works with language on English). 1075 | if (item.type != null && item.type.toLowerCase().includes('trading card')) { 1076 | return true; 1077 | } 1078 | 1079 | return false; 1080 | } 1081 | 1082 | function getIsFoilTradingCard(item) { 1083 | if (!getIsTradingCard(item)) { 1084 | return false; 1085 | } 1086 | 1087 | // This is available on the inventory page. 1088 | const tags = item.tags != null 1089 | ? item.tags 1090 | : item.description != null && item.description.tags != null 1091 | ? item.description.tags 1092 | : null; 1093 | if (tags != null) { 1094 | let isTaggedAsFoilTradingCard = false; 1095 | tags.forEach((arrayItem) => { 1096 | if (arrayItem.category == 'cardborder' && arrayItem.internal_name == 'cardborder_1') { // foil border. 1097 | isTaggedAsFoilTradingCard = true; 1098 | } 1099 | }); 1100 | if (isTaggedAsFoilTradingCard) { 1101 | return true; 1102 | } 1103 | } 1104 | 1105 | // This is available on the market page. 1106 | if (item.owner_actions != null) { 1107 | for (let i = 0; i < item.owner_actions.length; i++) { 1108 | if (item.owner_actions[i].link == null) { 1109 | continue; 1110 | } 1111 | 1112 | // Cards include a link to the gamecard page. 1113 | // The border parameter specifies the foil cards. 1114 | // For example: "http://steamcommunity.com/my/gamecards/503820/?border=1". 1115 | if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards') && 1116 | item.owner_actions[i].link.toString().toLowerCase().includes('border')) { 1117 | return true; 1118 | } 1119 | } 1120 | } 1121 | 1122 | // A fallback for the market page (only works with language on English). 1123 | if (item.type != null && item.type.toLowerCase().includes('foil trading card')) { 1124 | return true; 1125 | } 1126 | 1127 | return false; 1128 | } 1129 | 1130 | function CalculateFeeAmount(amount, publisherFee, walletInfo) { 1131 | if (walletInfo == null || !walletInfo['wallet_fee']) { 1132 | return { 1133 | fees: 0 1134 | }; 1135 | } 1136 | 1137 | publisherFee = publisherFee == null ? 0 : publisherFee; 1138 | // Since CalculateFeeAmount has a Math.floor, we could be off a cent or two. Let's check: 1139 | let iterations = 0; // shouldn't be needed, but included to be sure nothing unforseen causes us to get stuck 1140 | let nEstimatedAmountOfWalletFundsReceivedByOtherParty = 1141 | parseInt((amount - parseInt(walletInfo['wallet_fee_base'])) / 1142 | (parseFloat(walletInfo['wallet_fee_percent']) + parseFloat(publisherFee) + 1)); 1143 | let bEverUndershot = false; 1144 | let fees = CalculateAmountToSendForDesiredReceivedAmount( 1145 | nEstimatedAmountOfWalletFundsReceivedByOtherParty, 1146 | publisherFee, 1147 | walletInfo 1148 | ); 1149 | while (fees.amount != amount && iterations < 10) { 1150 | if (fees.amount > amount) { 1151 | if (bEverUndershot) { 1152 | fees = CalculateAmountToSendForDesiredReceivedAmount( 1153 | nEstimatedAmountOfWalletFundsReceivedByOtherParty - 1, 1154 | publisherFee, 1155 | walletInfo 1156 | ); 1157 | fees.steam_fee += amount - fees.amount; 1158 | fees.fees += amount - fees.amount; 1159 | fees.amount = amount; 1160 | break; 1161 | } else { 1162 | nEstimatedAmountOfWalletFundsReceivedByOtherParty--; 1163 | } 1164 | } else { 1165 | bEverUndershot = true; 1166 | nEstimatedAmountOfWalletFundsReceivedByOtherParty++; 1167 | } 1168 | fees = CalculateAmountToSendForDesiredReceivedAmount( 1169 | nEstimatedAmountOfWalletFundsReceivedByOtherParty, 1170 | publisherFee, 1171 | walletInfo 1172 | ); 1173 | iterations++; 1174 | } 1175 | // fees.amount should equal the passed in amount 1176 | return fees; 1177 | } 1178 | 1179 | // Clamps cur between min and max (inclusive). 1180 | function clamp(cur, min, max) { 1181 | if (cur < min) { 1182 | cur = min; 1183 | } 1184 | 1185 | if (cur > max) { 1186 | cur = max; 1187 | } 1188 | 1189 | return cur; 1190 | } 1191 | 1192 | // Strangely named function, it actually works out the fees and buyer price for a seller price 1193 | function CalculateAmountToSendForDesiredReceivedAmount(receivedAmount, publisherFee, walletInfo) { 1194 | if (walletInfo == null || !walletInfo['wallet_fee']) { 1195 | return { 1196 | amount: receivedAmount 1197 | }; 1198 | } 1199 | 1200 | publisherFee = publisherFee == null ? 0 : publisherFee; 1201 | const nSteamFee = parseInt(Math.floor(Math.max( 1202 | receivedAmount * parseFloat(walletInfo['wallet_fee_percent']), 1203 | walletInfo['wallet_fee_minimum'] 1204 | ) + 1205 | parseInt(walletInfo['wallet_fee_base']))); 1206 | const nPublisherFee = parseInt(Math.floor(publisherFee > 0 ? Math.max(receivedAmount * publisherFee, 1) : 0)); 1207 | const nAmountToSend = receivedAmount + nSteamFee + nPublisherFee; 1208 | return { 1209 | steam_fee: nSteamFee, 1210 | publisher_fee: nPublisherFee, 1211 | fees: nSteamFee + nPublisherFee, 1212 | amount: parseInt(nAmountToSend) 1213 | }; 1214 | } 1215 | 1216 | function readCookie(name) { 1217 | const nameEQ = `${name}=`; 1218 | const ca = document.cookie.split(';'); 1219 | for (let i = 0; i < ca.length; i++) { 1220 | let c = ca[i]; 1221 | while (c.charAt(0) == ' ') { 1222 | c = c.substring(1, c.length); 1223 | } 1224 | if (c.indexOf(nameEQ) == 0) { 1225 | return decodeURIComponent(c.substring(nameEQ.length, c.length)); 1226 | } 1227 | } 1228 | return null; 1229 | } 1230 | 1231 | function isRetryMessage(message) { 1232 | const messageList = [ 1233 | "在上一个操作完成之前,您不能出售任何物品。", 1234 | "列出您的物品时出现问题。刷新页面并重试。", 1235 | "我们无法连接到游戏物品服务器。游戏物品服务器可能已经关闭,或 Steam 可能正面临临时连接问题。您的列表尚未创建。请刷新页面并重试。" 1236 | ]; 1237 | 1238 | return messageList.indexOf(message) !== -1; 1239 | } 1240 | //#endregion 1241 | 1242 | //#region Logging 1243 | let userScrolled = false; 1244 | const logger = document.createElement('div'); 1245 | logger.setAttribute('id', 'logger'); 1246 | 1247 | function updateScroll() { 1248 | if (!userScrolled) { 1249 | const element = document.getElementById('logger'); 1250 | element.scrollTop = element.scrollHeight; 1251 | } 1252 | } 1253 | 1254 | function logDOM(text) { 1255 | logger.innerHTML += `${text}
`; 1256 | 1257 | updateScroll(); 1258 | } 1259 | 1260 | function logConsole(text) { 1261 | if (enableConsoleLog) { 1262 | console.log(text); 1263 | } 1264 | } 1265 | //#endregion 1266 | 1267 | //#region Inventory 1268 | if (currentPage == PAGE_INVENTORY) { 1269 | 1270 | function onQueueDrain() { 1271 | if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0 && boosterQueue.length() == 0) { 1272 | $('#inventory_items_spinner').remove(); 1273 | } 1274 | } 1275 | 1276 | function updateTotals() { 1277 | if ($('#loggerTotal').length == 0) { 1278 | $(logger).parent().append('
'); 1279 | } 1280 | 1281 | const totals = document.getElementById('loggerTotal'); 1282 | totals.innerHTML = ''; 1283 | 1284 | if (totalPriceWithFeesOnMarket > 0) { 1285 | totals.innerHTML += `
累计上架物品总价为 ${formatPrice(totalPriceWithFeesOnMarket)},你将会获得 ${formatPrice(totalPriceWithoutFeesOnMarket)}。
`; 1286 | } 1287 | if (totalScrap > 0) { 1288 | totals.innerHTML += `
总共分解:${totalScrap}
`; 1289 | } 1290 | } 1291 | 1292 | const sellQueue = async.queue( 1293 | (task, next) => { 1294 | totalNumberOfProcessedQueueItems++; 1295 | 1296 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1297 | const itemId = task.item.assetid || task.item.id; 1298 | const itemName = task.item.name || task.item.description.name; 1299 | const itemNameWithAmount = task.item.amount == 1 ? itemName : `${task.item.amount}x ${itemName}`; 1300 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1301 | 1302 | if (getSettingWithDefault(SETTING_PRICE_MIN_LIST_PRICE) * 100 >= market.getPriceIncludingFees(task.sellPrice)) { 1303 | logDOM(`由于价格忽略设置,${padLeft} - ${itemNameWithAmount} 未能上架。`); 1304 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_PRICE_NOT_CHECKED); 1305 | next(); 1306 | return; 1307 | } 1308 | 1309 | market.sellItem( 1310 | task.item, 1311 | task.sellPrice, 1312 | (error, data) => { 1313 | const success = Boolean(data?.success); 1314 | const message = data?.message || ''; 1315 | 1316 | const callback = () => setTimeout(() => next(), getRandomInt(1000, 1500)); 1317 | 1318 | if (success) { 1319 | logDOM(`${padLeft} - ${itemNameWithAmount} 已添加至市场,售价为 ${formatPrice(market.getPriceIncludingFees(task.sellPrice) * task.item.amount)},你将收到 ${formatPrice(task.sellPrice * task.item.amount)}。`); 1320 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1321 | 1322 | totalPriceWithoutFeesOnMarket += task.sellPrice * task.item.amount; 1323 | totalPriceWithFeesOnMarket += market.getPriceIncludingFees(task.sellPrice) * task.item.amount; 1324 | 1325 | updateTotals(); 1326 | callback() 1327 | 1328 | return; 1329 | } 1330 | 1331 | if (message && isRetryMessage(message)) { 1332 | logDOM(`${padLeft} - ${itemNameWithAmount} 正在重试列出物品,原因为 ${message.charAt(0).toLowerCase()}${message.slice(1)}`); 1333 | 1334 | totalNumberOfProcessedQueueItems--; 1335 | sellQueue.unshift(task); 1336 | sellQueue.pause(); 1337 | 1338 | setTimeout(() => sellQueue.resume(), getRandomInt(30000, 45000)); 1339 | callback(); 1340 | 1341 | return; 1342 | } 1343 | 1344 | logDOM(`${padLeft} - ${itemNameWithAmount} 上架市场失败${message ? `,原因为 ${message.charAt(0).toLowerCase()}${message.slice(1)}` : '。'}`); 1345 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1346 | 1347 | callback(); 1348 | } 1349 | ); 1350 | }, 1351 | 1 1352 | ); 1353 | 1354 | sellQueue.drain(() => { 1355 | onQueueDrain(); 1356 | }); 1357 | 1358 | function sellAllItems() { 1359 | loadAllInventories().then( 1360 | () => { 1361 | const items = getInventoryItems(); 1362 | const filteredItems = []; 1363 | 1364 | items.forEach((item) => { 1365 | if (!item.marketable) { 1366 | return; 1367 | } 1368 | 1369 | filteredItems.push(item); 1370 | }); 1371 | 1372 | sellItems(filteredItems); 1373 | }, 1374 | () => { 1375 | logDOM('无法检索库存...'); 1376 | } 1377 | ); 1378 | } 1379 | 1380 | function sellAllDuplicateItems() { 1381 | loadAllInventories().then( 1382 | () => { 1383 | const items = getInventoryItems(); 1384 | const marketableItems = []; 1385 | let filteredItems = []; 1386 | 1387 | items.forEach((item) => { 1388 | if (!item.marketable) { 1389 | return; 1390 | } 1391 | 1392 | marketableItems.push(item); 1393 | }); 1394 | 1395 | filteredItems = marketableItems.filter((e, i) => marketableItems.map((m) => m.classid).indexOf(e.classid) !== i); 1396 | 1397 | sellItems(filteredItems); 1398 | }, 1399 | () => { 1400 | logDOM('无法检索库存...'); 1401 | } 1402 | ); 1403 | } 1404 | 1405 | function gemAllDuplicateItems() { 1406 | loadAllInventories().then( 1407 | () => { 1408 | const items = getInventoryItems(); 1409 | let filteredItems = []; 1410 | let numberOfQueuedItems = 0; 1411 | 1412 | filteredItems = items.filter((e, i) => items.map((m) => m.classid).indexOf(e.classid) !== i); 1413 | 1414 | filteredItems.forEach((item) => { 1415 | if (item.queued != null) { 1416 | return; 1417 | } 1418 | 1419 | if (item.owner_actions == null) { 1420 | return; 1421 | } 1422 | 1423 | let canTurnIntoGems = false; 1424 | for (const owner_action in item.owner_actions) { 1425 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 1426 | canTurnIntoGems = true; 1427 | } 1428 | } 1429 | 1430 | if (!canTurnIntoGems) { 1431 | return; 1432 | } 1433 | 1434 | item.queued = true; 1435 | scrapQueue.push(item); 1436 | numberOfQueuedItems++; 1437 | }); 1438 | 1439 | if (numberOfQueuedItems > 0) { 1440 | totalNumberOfQueuedItems += numberOfQueuedItems; 1441 | 1442 | $('#inventory_items_spinner').remove(); 1443 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1444 | }
正在处理处理 ${numberOfQueuedItems} 个物品
` + 1445 | '
'); 1446 | } 1447 | }, 1448 | () => { 1449 | logDOM('无法检索库存...'); 1450 | } 1451 | ); 1452 | } 1453 | 1454 | function sellAllCards() { 1455 | loadAllInventories().then( 1456 | () => { 1457 | const items = getInventoryItems(); 1458 | const filteredItems = []; 1459 | 1460 | items.forEach((item) => { 1461 | if (!getIsTradingCard(item) || !item.marketable) { 1462 | return; 1463 | } 1464 | 1465 | filteredItems.push(item); 1466 | }); 1467 | 1468 | sellItems(filteredItems); 1469 | }, 1470 | () => { 1471 | logDOM('无法检索库存...'); 1472 | } 1473 | ); 1474 | } 1475 | 1476 | function sellAllCrates() { 1477 | loadAllInventories().then( 1478 | () => { 1479 | const items = getInventoryItems(); 1480 | const filteredItems = []; 1481 | items.forEach((item) => { 1482 | if (!getIsCrate(item) || !item.marketable) { 1483 | return; 1484 | } 1485 | filteredItems.push(item); 1486 | }); 1487 | 1488 | sellItems(filteredItems); 1489 | }, 1490 | () => { 1491 | logDOM('无法检索库存...'); 1492 | } 1493 | ); 1494 | } 1495 | 1496 | const scrapQueue = async.queue((item, next) => { 1497 | scrapQueueWorker(item, (success) => { 1498 | if (success) { 1499 | setTimeout(() => { 1500 | next(); 1501 | }, 250); 1502 | } else { 1503 | const delay = numberOfFailedRequests > 1 1504 | ? getRandomInt(30000, 45000) 1505 | : getRandomInt(1000, 1500); 1506 | 1507 | if (numberOfFailedRequests > 3) { 1508 | numberOfFailedRequests = 0; 1509 | } 1510 | 1511 | setTimeout(() => { 1512 | next(); 1513 | }, delay); 1514 | } 1515 | }); 1516 | }, 1); 1517 | 1518 | scrapQueue.drain(() => { 1519 | onQueueDrain(); 1520 | }); 1521 | 1522 | function scrapQueueWorker(item, callback) { 1523 | const itemName = item.name || item.description.name; 1524 | const itemId = item.assetid || item.id; 1525 | 1526 | market.getGooValue( 1527 | item, 1528 | (err, goo) => { 1529 | totalNumberOfProcessedQueueItems++; 1530 | 1531 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1532 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1533 | 1534 | if (err != ERROR_SUCCESS) { 1535 | logConsole(`无法获取 ${itemName} 分解后的宝石数。`); 1536 | logDOM(`${padLeft} - ${itemName} 由于缺少宝石数,而未分解为宝石。`); 1537 | 1538 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1539 | return callback(false); 1540 | } 1541 | 1542 | item.goo_value_expected = parseInt(goo.goo_value, 10); 1543 | 1544 | market.grindIntoGoo( 1545 | item, 1546 | (err) => { 1547 | if (err != ERROR_SUCCESS) { 1548 | logConsole(`无法将 ${itemName} 分解为宝石。`); 1549 | logDOM(`${padLeft} - ${itemName} 由于未知错误,未分解为宝石。`); 1550 | 1551 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1552 | return callback(false); 1553 | } 1554 | 1555 | logConsole('============================'); 1556 | logConsole(itemName); 1557 | logConsole(`分解为 ${goo.goo_value} 个宝石`); 1558 | logDOM(`${padLeft} - ${itemName} 已分解为 ${item.goo_value_expected} 个宝石。`); 1559 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1560 | 1561 | totalScrap += item.goo_value_expected; 1562 | updateTotals(); 1563 | 1564 | callback(true); 1565 | } 1566 | ); 1567 | } 1568 | ); 1569 | } 1570 | 1571 | const boosterQueue = async.queue((item, next) => { 1572 | boosterQueueWorker(item, (success) => { 1573 | if (success) { 1574 | setTimeout(() => { 1575 | next(); 1576 | }, 250); 1577 | } else { 1578 | const delay = numberOfFailedRequests > 1 1579 | ? getRandomInt(30000, 45000) 1580 | : getRandomInt(1000, 1500); 1581 | 1582 | if (numberOfFailedRequests > 3) { 1583 | numberOfFailedRequests = 0; 1584 | } 1585 | 1586 | setTimeout(() => { 1587 | next(); 1588 | }, delay); 1589 | } 1590 | }); 1591 | }, 1); 1592 | 1593 | boosterQueue.drain(() => { 1594 | onQueueDrain(); 1595 | }); 1596 | 1597 | function boosterQueueWorker(item, callback) { 1598 | const itemName = item.name || item.description.name; 1599 | const itemId = item.assetid || item.id; 1600 | 1601 | market.unpackBoosterPack( 1602 | item, 1603 | (err) => { 1604 | totalNumberOfProcessedQueueItems++; 1605 | 1606 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1607 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1608 | 1609 | if (err != ERROR_SUCCESS) { 1610 | logConsole(`无法拆开补充包 ${itemName}`); 1611 | logDOM(`${padLeft} - ${itemName} 拆包失败。`); 1612 | 1613 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1614 | return callback(false); 1615 | } 1616 | 1617 | logDOM(`${padLeft} - ${itemName} 拆包成功。`); 1618 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1619 | 1620 | callback(true); 1621 | } 1622 | ); 1623 | } 1624 | 1625 | 1626 | // Turns the selected items into gems. 1627 | function turnSelectedItemsIntoGems() { 1628 | const ids = getSelectedItems(); 1629 | 1630 | loadAllInventories().then(() => { 1631 | const items = getInventoryItems(); 1632 | 1633 | let numberOfQueuedItems = 0; 1634 | items.forEach((item) => { 1635 | // Ignored queued items. 1636 | if (item.queued != null) { 1637 | return; 1638 | } 1639 | 1640 | if (item.owner_actions == null) { 1641 | return; 1642 | } 1643 | 1644 | let canTurnIntoGems = false; 1645 | for (const owner_action in item.owner_actions) { 1646 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 1647 | canTurnIntoGems = true; 1648 | } 1649 | } 1650 | 1651 | if (!canTurnIntoGems) { 1652 | return; 1653 | } 1654 | 1655 | const itemId = item.assetid || item.id; 1656 | if (ids.indexOf(itemId) !== -1) { 1657 | item.queued = true; 1658 | scrapQueue.push(item); 1659 | numberOfQueuedItems++; 1660 | } 1661 | }); 1662 | 1663 | if (numberOfQueuedItems > 0) { 1664 | totalNumberOfQueuedItems += numberOfQueuedItems; 1665 | 1666 | $('#inventory_items_spinner').remove(); 1667 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1668 | }
正在处理 ${numberOfQueuedItems} 个物品
` + 1669 | '
'); 1670 | } 1671 | }, () => { 1672 | logDOM('无法检索库存...'); 1673 | }); 1674 | } 1675 | 1676 | // Unpacks the selected booster packs. 1677 | function unpackSelectedBoosterPacks() { 1678 | const ids = getSelectedItems(); 1679 | 1680 | loadAllInventories().then(() => { 1681 | const items = getInventoryItems(); 1682 | 1683 | let numberOfQueuedItems = 0; 1684 | items.forEach((item) => { 1685 | // Ignored queued items. 1686 | if (item.queued != null) { 1687 | return; 1688 | } 1689 | 1690 | if (item.owner_actions == null) { 1691 | return; 1692 | } 1693 | 1694 | let canOpenBooster = false; 1695 | for (const owner_action in item.owner_actions) { 1696 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { 1697 | canOpenBooster = true; 1698 | } 1699 | } 1700 | 1701 | if (!canOpenBooster) { 1702 | return; 1703 | } 1704 | 1705 | const itemId = item.assetid || item.id; 1706 | if (ids.indexOf(itemId) !== -1) { 1707 | item.queued = true; 1708 | boosterQueue.push(item); 1709 | numberOfQueuedItems++; 1710 | } 1711 | }); 1712 | 1713 | if (numberOfQueuedItems > 0) { 1714 | totalNumberOfQueuedItems += numberOfQueuedItems; 1715 | 1716 | $('#inventory_items_spinner').remove(); 1717 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1718 | }
正在处理 ${numberOfQueuedItems} 个物品
` + 1719 | '
'); 1720 | } 1721 | }, () => { 1722 | logDOM('无法检索库存...'); 1723 | }); 1724 | } 1725 | 1726 | function sellSelectedItems() { 1727 | getInventorySelectedMarketableItems((items) => { 1728 | sellItems(items); 1729 | }); 1730 | } 1731 | 1732 | function canSellSelectedItemsManually(items) { 1733 | // We have to construct an URL like this 1734 | // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 1735 | const contextid = items[0].contextid; 1736 | let hasInvalidItem = false; 1737 | 1738 | items.forEach((item) => { 1739 | if (item.contextid != contextid || item.commodity == false) { 1740 | hasInvalidItem = true; 1741 | } 1742 | }); 1743 | 1744 | return !hasInvalidItem; 1745 | } 1746 | 1747 | function sellSelectedItemsManually() { 1748 | getInventorySelectedMarketableItems((items) => { 1749 | // We have to construct an URL like this 1750 | // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 1751 | 1752 | const appid = items[0].appid; 1753 | const contextid = items[0].contextid; 1754 | 1755 | const itemsWithQty = {}; 1756 | 1757 | items.forEach((item) => { 1758 | itemsWithQty[item.market_hash_name] = itemsWithQty[item.market_hash_name] + 1 || 1; 1759 | }); 1760 | 1761 | let itemsString = ''; 1762 | for (const itemName in itemsWithQty) { 1763 | itemsString += `&items[]=${encodeURIComponent(itemName)}&qty[]=${itemsWithQty[itemName]}`; 1764 | } 1765 | 1766 | const baseUrl = `${window.location.origin}/market/multisell`; 1767 | const redirectUrl = `${baseUrl}?appid=${appid}&contextid=${contextid}${itemsString}`; 1768 | 1769 | const dialog = unsafeWindow.ShowDialog('Steam Economy Enhancer', ``); 1770 | dialog.OnDismiss(() => { 1771 | items.forEach((item) => { 1772 | const itemId = item.assetid || item.id; 1773 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_PENDING); 1774 | }); 1775 | }); 1776 | }); 1777 | } 1778 | 1779 | function sellItems(items) { 1780 | if (items.length == 0) { 1781 | logDOM('这些物品无法被上架至市场...'); 1782 | 1783 | return; 1784 | } 1785 | 1786 | let numberOfQueuedItems = 0; 1787 | 1788 | items.forEach((item) => { 1789 | // Ignored queued items. 1790 | if (item.queued != null) { 1791 | return; 1792 | } 1793 | 1794 | item.queued = true; 1795 | item.ignoreErrors = false; 1796 | itemQueue.push(item); 1797 | numberOfQueuedItems++; 1798 | }); 1799 | 1800 | if (numberOfQueuedItems > 0) { 1801 | totalNumberOfQueuedItems += numberOfQueuedItems; 1802 | 1803 | $('#inventory_items_spinner').remove(); 1804 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1805 | }
正在处理 ${numberOfQueuedItems} 个物品
` + 1806 | '
'); 1807 | } 1808 | } 1809 | 1810 | const itemQueue = async.queue((item, next) => { 1811 | itemQueueWorker( 1812 | item, 1813 | item.ignoreErrors, 1814 | (success, cached) => { 1815 | if (success) { 1816 | setTimeout(() => next(), cached ? 0 : getRandomInt(1000, 1500)); 1817 | } else { 1818 | if (!item.ignoreErrors) { 1819 | item.ignoreErrors = true; 1820 | itemQueue.push(item); 1821 | } 1822 | 1823 | const delay = numberOfFailedRequests > 1 ? getRandomInt(30000, 45000) : getRandomInt(1000, 1500); 1824 | numberOfFailedRequests = numberOfFailedRequests > 3 ? 0 : numberOfFailedRequests; 1825 | 1826 | setTimeout(() => next(), cached ? 0 : delay); 1827 | } 1828 | } 1829 | ); 1830 | }, 1); 1831 | 1832 | function itemQueueWorker(item, ignoreErrors, callback) { 1833 | const priceInfo = getPriceInformationFromItem(item); 1834 | 1835 | let failed = 0; 1836 | const itemName = item.name || item.description.name; 1837 | 1838 | market.getPriceHistory( 1839 | item, 1840 | true, 1841 | (err, history, cachedHistory) => { 1842 | if (err) { 1843 | logConsole(`无法获取 ${itemName} 的价格历史`); 1844 | 1845 | if (err != ERROR_SUCCESS) { 1846 | failed += 1; 1847 | } 1848 | } 1849 | 1850 | market.getItemOrdersHistogram( 1851 | item, 1852 | true, 1853 | (err, histogram, cachedListings) => { 1854 | if (err) { 1855 | logConsole(`无法获取 ${itemName} 的订单直方图`); 1856 | 1857 | if (err != ERROR_SUCCESS) { 1858 | failed += 1; 1859 | } 1860 | } 1861 | 1862 | if (failed > 0 && !ignoreErrors) { 1863 | return callback(false, cachedHistory && cachedListings); 1864 | } 1865 | 1866 | logConsole('============================'); 1867 | logConsole(itemName); 1868 | 1869 | const sellPrice = calculateSellPriceBeforeFees( 1870 | history, 1871 | histogram, 1872 | true, 1873 | priceInfo.minPriceBeforeFees, 1874 | priceInfo.maxPriceBeforeFees 1875 | ); 1876 | 1877 | 1878 | logConsole(`售价:${sellPrice / 100.0} (${market.getPriceIncludingFees(sellPrice) / 100.0})`); 1879 | 1880 | sellQueue.push({ 1881 | item: item, 1882 | sellPrice: sellPrice 1883 | }); 1884 | 1885 | return callback(true, cachedHistory && cachedListings); 1886 | } 1887 | ); 1888 | } 1889 | ); 1890 | } 1891 | 1892 | // Initialize the inventory UI. 1893 | function initializeInventoryUI() { 1894 | const isOwnInventory = unsafeWindow.g_ActiveUser.strSteamId == unsafeWindow.g_steamID; 1895 | let previousSelection = -1; // To store the index of the previous selection. 1896 | updateInventoryUI(isOwnInventory); 1897 | 1898 | $('.games_list_tabs').on( 1899 | 'click', 1900 | '*', 1901 | () => { 1902 | updateInventoryUI(isOwnInventory); 1903 | } 1904 | ); 1905 | 1906 | // Ignore selection on other user's inventories. 1907 | if (!isOwnInventory) { 1908 | return; 1909 | } 1910 | 1911 | // Steam adds 'display:none' to items while searching. These should not be selected while using shift/ctrl. 1912 | const filter = '.itemHolder:not([style*=none])'; 1913 | $('#inventories').selectable({ 1914 | filter: filter, 1915 | selecting: function (e, ui) { 1916 | // Get selected item index. 1917 | const selectedIndex = $(ui.selecting.tagName, e.target).index(ui.selecting); 1918 | 1919 | // If shift key was pressed and there is previous - select them all. 1920 | if (e.shiftKey && previousSelection > -1) { 1921 | $(ui.selecting.tagName, e.target). 1922 | slice( 1923 | Math.min(previousSelection, selectedIndex), 1924 | 1 + Math.max(previousSelection, selectedIndex) 1925 | ).each(function () { 1926 | if ($(this).is(filter)) { 1927 | $(this).addClass('ui-selected'); 1928 | } 1929 | }); 1930 | previousSelection = -1; // Reset previous. 1931 | } else { 1932 | previousSelection = selectedIndex; // Save previous. 1933 | } 1934 | }, 1935 | selected: function () { 1936 | updateButtons(); 1937 | } 1938 | }); 1939 | 1940 | if (typeof unsafeWindow.CInventory !== 'undefined') { 1941 | const originalSelectItem = unsafeWindow.CInventory.prototype.SelectItem; 1942 | 1943 | unsafeWindow.CInventory.prototype.SelectItem = function (event, elItem, rgItem) { 1944 | originalSelectItem.apply(this, arguments); 1945 | 1946 | updateButtons(); 1947 | updateInventorySelection(rgItem); 1948 | }; 1949 | } 1950 | } 1951 | 1952 | // Gets the selected items in the inventory. 1953 | function getSelectedItems() { 1954 | const ids = []; 1955 | $('.inventory_ctn').each(function () { 1956 | $(this).find('.inventory_page').each(function () { 1957 | const inventory_page = this; 1958 | 1959 | $(inventory_page).find('.itemHolder.ui-selected:not([style*=none])').each(function () { 1960 | $(this).find('.item').each(function () { 1961 | const matches = this.id.match(/_(-?\d+)$/); 1962 | if (matches) { 1963 | ids.push(matches[1]); 1964 | } 1965 | }); 1966 | }); 1967 | }); 1968 | }); 1969 | 1970 | return ids; 1971 | } 1972 | 1973 | // Gets the selected and marketable items in the inventory. 1974 | function getInventorySelectedMarketableItems(callback) { 1975 | const ids = getSelectedItems(); 1976 | 1977 | loadAllInventories().then(() => { 1978 | const items = getInventoryItems(); 1979 | const filteredItems = []; 1980 | 1981 | items.forEach((item) => { 1982 | if (!item.marketable) { 1983 | return; 1984 | } 1985 | 1986 | const itemId = item.assetid || item.id; 1987 | if (ids.indexOf(itemId) !== -1) { 1988 | filteredItems.push(item); 1989 | } 1990 | }); 1991 | 1992 | callback(filteredItems); 1993 | }, () => { 1994 | logDOM('无法检索库存...'); 1995 | }); 1996 | } 1997 | 1998 | // Gets the selected and gemmable items in the inventory. 1999 | function getInventorySelectedGemsItems(callback) { 2000 | const ids = getSelectedItems(); 2001 | 2002 | loadAllInventories().then(() => { 2003 | const items = getInventoryItems(); 2004 | const filteredItems = []; 2005 | 2006 | items.forEach((item) => { 2007 | let canTurnIntoGems = false; 2008 | for (const owner_action in item.owner_actions) { 2009 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 2010 | canTurnIntoGems = true; 2011 | } 2012 | } 2013 | 2014 | if (!canTurnIntoGems) { 2015 | return; 2016 | } 2017 | 2018 | const itemId = item.assetid || item.id; 2019 | if (ids.indexOf(itemId) !== -1) { 2020 | filteredItems.push(item); 2021 | } 2022 | }); 2023 | 2024 | callback(filteredItems); 2025 | }, () => { 2026 | logDOM('无法检索库存...'); 2027 | }); 2028 | } 2029 | 2030 | // Gets the selected and booster pack items in the inventory. 2031 | function getInventorySelectedBoosterPackItems(callback) { 2032 | const ids = getSelectedItems(); 2033 | 2034 | loadAllInventories().then(() => { 2035 | const items = getInventoryItems(); 2036 | const filteredItems = []; 2037 | 2038 | items.forEach((item) => { 2039 | let canOpenBooster = false; 2040 | for (const owner_action in item.owner_actions) { 2041 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { 2042 | canOpenBooster = true; 2043 | } 2044 | } 2045 | 2046 | if (!canOpenBooster) { 2047 | return; 2048 | } 2049 | 2050 | const itemId = item.assetid || item.id; 2051 | if (ids.indexOf(itemId) !== -1) { 2052 | filteredItems.push(item); 2053 | } 2054 | }); 2055 | 2056 | callback(filteredItems); 2057 | }, () => { 2058 | logDOM('无法检索库存...'); 2059 | }); 2060 | } 2061 | 2062 | // Updates the (selected) sell ... items button. 2063 | function updateSellSelectedButton() { 2064 | getInventorySelectedMarketableItems((items) => { 2065 | const selectedItems = items.length; 2066 | if (items.length == 0) { 2067 | $('.sell_selected').hide(); 2068 | $('.sell_manual').hide(); 2069 | } else { 2070 | $('.sell_selected').show(); 2071 | if (canSellSelectedItemsManually(items)) { 2072 | $('.sell_manual').show(); 2073 | $('.sell_manual > span').text(`手动出售 ${selectedItems} 个物品`); 2074 | } else { 2075 | $('.sell_manual').hide(); 2076 | } 2077 | $('.sell_selected > span').text(`出售 ${selectedItems} 个物品`); 2078 | } 2079 | }); 2080 | } 2081 | 2082 | // Updates the (selected) turn into ... gems button. 2083 | function updateTurnIntoGemsButton() { 2084 | getInventorySelectedGemsItems((items) => { 2085 | const selectedItems = items.length; 2086 | if (items.length == 0) { 2087 | $('.turn_into_gems').hide(); 2088 | } else { 2089 | $('.turn_into_gems').show(); 2090 | $('.turn_into_gems > span'). 2091 | text(`分解 ${selectedItems} 个物品为宝石`); 2092 | } 2093 | }); 2094 | } 2095 | 2096 | // Updates the (selected) open ... booster packs button. 2097 | function updateOpenBoosterPacksButton() { 2098 | getInventorySelectedBoosterPackItems((items) => { 2099 | const selectedItems = items.length; 2100 | if (items.length == 0) { 2101 | $('.unpack_booster_packs').hide(); 2102 | } else { 2103 | $('.unpack_booster_packs').show(); 2104 | $('.unpack_booster_packs > span'). 2105 | text(`拆开 ${selectedItems} 个补充包`); 2106 | } 2107 | }); 2108 | } 2109 | 2110 | function updateButtons() { 2111 | updateSellSelectedButton(); 2112 | updateTurnIntoGemsButton(); 2113 | updateOpenBoosterPacksButton(); 2114 | } 2115 | 2116 | function updateInventorySelection(selectedItem) { 2117 | const item_info = $(`#iteminfo${unsafeWindow.iActiveSelectView}`); 2118 | 2119 | if (!item_info.length) { 2120 | return; 2121 | } 2122 | 2123 | if (item_info.html().indexOf('checkout/sendgift/') > -1) { // Gifts have no market information. 2124 | return; 2125 | } 2126 | 2127 | // Use a 'hard' item id instead of relying on the selected item_info (sometimes Steam temporarily changes the correct item (?)). 2128 | const item_info_id = item_info.attr('id'); 2129 | 2130 | // Move scrap to bottom, this is of little interest. 2131 | const scrap = $(`#${item_info_id}_scrap_content`); 2132 | scrap.next().insertBefore(scrap); 2133 | 2134 | // Skip unmarketable items 2135 | if (!selectedItem.marketable) { 2136 | return; 2137 | } 2138 | 2139 | // Starting at prices are already retrieved in the table. 2140 | //$('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(2)') 2141 | // .remove(); // Starting at: x,xx. 2142 | 2143 | const market_hash_name = getMarketHashName(selectedItem); 2144 | if (market_hash_name == null) { 2145 | return; 2146 | } 2147 | 2148 | const appid = selectedItem.appid; 2149 | const item = { 2150 | appid: parseInt(appid), 2151 | description: { 2152 | market_hash_name: market_hash_name 2153 | } 2154 | }; 2155 | 2156 | const ownerActions = $(`#${item_info_id}_item_owner_actions`); 2157 | 2158 | // Move market link to a button 2159 | ownerActions.append(`在社区市场中查看`); 2160 | $(`#${item_info_id}_item_market_actions > div:nth-child(1) > div:nth-child(1)`).hide(); 2161 | 2162 | // ownerActions is hidden on other games' inventories, we need to show it to have a "Market" button visible 2163 | ownerActions.show(); 2164 | 2165 | const isBoosterPack = selectedItem.name.toLowerCase().endsWith('booster pack'); 2166 | if (isBoosterPack) { 2167 | const tradingCardsUrl = `/market/search?q=&category_753_Game%5B%5D=tag_app_${selectedItem.market_fee_app}&category_753_item_class%5B%5D=tag_item_class_2&appid=753`; 2168 | ownerActions.append(`
View trading cards in Community Market`); 2169 | } 2170 | 2171 | if (getSettingWithDefault(SETTING_QUICK_SELL_BUTTONS) != 1) { 2172 | return; 2173 | } 2174 | 2175 | // Ignored queued items. 2176 | if (selectedItem.queued != null) { 2177 | return; 2178 | } 2179 | 2180 | market.getItemOrdersHistogram( 2181 | item, 2182 | false, 2183 | (err, histogram) => { 2184 | if (err) { 2185 | logConsole(`无法获取 ${selectedItem.name || selectedItem.description.name} 的订单直方图`); 2186 | return; 2187 | } 2188 | 2189 | // Ignored queued items. 2190 | if (selectedItem.queued != null) { 2191 | return; 2192 | } 2193 | 2194 | const groupMain = $(`
2195 |
2196 |
出售
2197 | ${histogram.sell_order_table} 2198 |
2199 |
2200 |
购买
2201 | ${histogram.buy_order_table} 2202 |
2203 |
`); 2204 | 2205 | $(`#${item_info_id}_item_market_actions > div`).after(groupMain); 2206 | 2207 | // Generate quick sell buttons. 2208 | let prices = []; 2209 | 2210 | if (histogram != null && histogram.highest_buy_order != null) { 2211 | prices.push(parseInt(histogram.highest_buy_order)); 2212 | } 2213 | 2214 | if (histogram != null && histogram.lowest_sell_order != null) { 2215 | // Transaction volume must be separable into three or more parts (no matter if equal): valve+publisher+seller. 2216 | if (parseInt(histogram.lowest_sell_order) > 3) { 2217 | prices.push(parseInt(histogram.lowest_sell_order) - 1); 2218 | } 2219 | prices.push(parseInt(histogram.lowest_sell_order)); 2220 | } 2221 | 2222 | prices = prices.filter((v, i) => prices.indexOf(v) === i).sort((a, b) => a - b); 2223 | 2224 | let buttons = ' '; 2225 | prices.forEach((e) => { 2226 | buttons += ` 2227 | 2228 | ${formatPrice(e)} 2229 | 2230 | 2231 | `; 2232 | }); 2233 | 2234 | $(`#${item_info_id}_item_market_actions`, item_info).append(buttons); 2235 | 2236 | $(`#${item_info_id}_item_market_actions`, item_info).append(``); 2245 | 2246 | $('.quick_sell').on( 2247 | 'click', 2248 | function () { 2249 | let price = $(this).attr('id').replace('quick_sell', ''); 2250 | price = market.getPriceBeforeFees(price); 2251 | 2252 | totalNumberOfQueuedItems++; 2253 | 2254 | sellQueue.push({ 2255 | item: selectedItem, 2256 | sellPrice: price 2257 | }); 2258 | } 2259 | ); 2260 | 2261 | $('.quick_sell_custom').on( 2262 | 'click', 2263 | () => { 2264 | let price = $('#quick_sell_input', $(`#${item_info_id}_item_market_actions`, item_info)).val() * 100; 2265 | price = market.getPriceBeforeFees(price); 2266 | 2267 | totalNumberOfQueuedItems++; 2268 | 2269 | sellQueue.push({ 2270 | item: selectedItem, 2271 | sellPrice: price 2272 | }); 2273 | } 2274 | ); 2275 | } 2276 | ); 2277 | } 2278 | 2279 | // Update the inventory UI. 2280 | function updateInventoryUI(isOwnInventory) { 2281 | // Remove previous containers (e.g., when a user changes inventory). 2282 | $('#inventory_sell_buttons').remove(); 2283 | $('#see_settings_modal').remove(); 2284 | $('#inventory_reload_button').remove(); 2285 | 2286 | $('#see_settings').remove(); 2287 | $('#global_action_menu'). 2288 | prepend('⬖ SEE 设置 '); 2289 | $('#see_settings').on('click', '*', () => openSettings()); 2290 | 2291 | const appId = getActiveInventory().m_appid; 2292 | const showMiscOptions = appId == 753; 2293 | const TF2 = appId == 440; 2294 | 2295 | let buttonsHtml = ` 2296 | 出售所有物品 2297 | 出售所有重复物品 2298 | 2299 | 2300 | `; 2301 | 2302 | if (showMiscOptions) { 2303 | buttonsHtml += ` 2304 | 出售所有卡牌 2305 | 2310 | `; 2311 | } else if (TF2) { 2312 | buttonsHtml += '出售所有箱子'; 2313 | } 2314 | 2315 | const sellButtons = $(`
${buttonsHtml}
`); 2316 | 2317 | const reloadButton = 2318 | $('重新加载库存'); 2319 | 2320 | const logo = $('#inventory_logos')[0]; 2321 | logo.style.height = 'auto'; 2322 | logo.style.maxHeight = 'unset'; 2323 | 2324 | $('#inventory_applogo').hide(); // Hide the Steam/game logo, we don't need to see it twice. 2325 | $('#inventory_applogo').after(logger); 2326 | 2327 | 2328 | $('#logger').on( 2329 | 'scroll', 2330 | () => { 2331 | const hasUserScrolledToBottom = 2332 | $('#logger').prop('scrollHeight') - $('#logger').prop('clientHeight') <= 2333 | $('#logger').prop('scrollTop') + 1; 2334 | userScrolled = !hasUserScrolledToBottom; 2335 | } 2336 | ); 2337 | 2338 | // Only add buttons on the user's inventory. 2339 | if (isOwnInventory) { 2340 | $('#inventory_applogo').after(sellButtons); 2341 | 2342 | // Add bindings to sell buttons. 2343 | $('.sell_all').on( 2344 | 'click', 2345 | '*', 2346 | () => { 2347 | sellAllItems(); 2348 | } 2349 | ); 2350 | $('.sell_selected').on('click', '*', sellSelectedItems); 2351 | $('.sell_all_duplicates').on('click', '*', sellAllDuplicateItems); 2352 | $('.gem_all_duplicates').on('click', '*', gemAllDuplicateItems); 2353 | $('.sell_manual').on('click', '*', sellSelectedItemsManually); 2354 | $('.sell_all_cards').on('click', '*', sellAllCards); 2355 | $('.sell_all_crates').on('click', '*', sellAllCrates); 2356 | $('.turn_into_gems').on('click', '*', turnSelectedItemsIntoGems); 2357 | $('.unpack_booster_packs').on('click', '*', unpackSelectedBoosterPacks); 2358 | 2359 | } 2360 | 2361 | $('.inventory_rightnav').prepend(reloadButton); 2362 | $('.reload_inventory').on( 2363 | 'click', 2364 | '*', 2365 | () => { 2366 | window.location.reload(); 2367 | } 2368 | ); 2369 | 2370 | loadAllInventories().then( 2371 | () => { 2372 | const updateInventoryPrices = function () { 2373 | if (getSettingWithDefault(SETTING_INVENTORY_PRICE_LABELS) == 1) { 2374 | setInventoryPrices(getInventoryItems()); 2375 | } 2376 | }; 2377 | 2378 | // Load after the inventory is loaded. 2379 | updateInventoryPrices(); 2380 | 2381 | $('#inventory_pagecontrols').observe( 2382 | 'childlist', 2383 | '*', 2384 | () => { 2385 | updateInventoryPrices(); 2386 | } 2387 | ); 2388 | }, 2389 | () => { 2390 | logDOM('无法检索库存...'); 2391 | } 2392 | ); 2393 | } 2394 | 2395 | // Loads the specified inventories. 2396 | function loadInventories(inventories) { 2397 | return new Promise((resolve) => { 2398 | inventories.reduce( 2399 | (promise, inventory) => { 2400 | return promise.then(() => { 2401 | // return inventory.LoadCompleteInventory().done(() => { }); 2402 | 2403 | // Workaround, until Steam fixes the issue with LoadCompleteInventory. 2404 | 2405 | if (inventory.m_bFullyLoaded) { 2406 | return Promise.resolve(); 2407 | } 2408 | 2409 | if (!inventory.m_promiseLoadCompleteInventory) { 2410 | inventory.m_promiseLoadCompleteInventory = inventory.LoadUntilConditionMet(() => inventory.m_bFullyLoaded, 2000); 2411 | } 2412 | 2413 | return inventory.m_promiseLoadCompleteInventory.done(() => { }); 2414 | }); 2415 | }, 2416 | Promise.resolve() 2417 | ); 2418 | 2419 | resolve(); 2420 | }); 2421 | } 2422 | 2423 | // Loads all inventories. 2424 | function loadAllInventories() { 2425 | const items = []; 2426 | 2427 | for (const child in getActiveInventory().m_rgChildInventories) { 2428 | items.push(getActiveInventory().m_rgChildInventories[child]); 2429 | } 2430 | items.push(getActiveInventory()); 2431 | 2432 | return loadInventories(items); 2433 | } 2434 | 2435 | // Gets the inventory items from the active inventory. 2436 | function getInventoryItems() { 2437 | const arr = []; 2438 | 2439 | for (const child in getActiveInventory().m_rgChildInventories) { 2440 | for (const key in getActiveInventory().m_rgChildInventories[child].m_rgAssets) { 2441 | const value = getActiveInventory().m_rgChildInventories[child].m_rgAssets[key]; 2442 | if (typeof value === 'object') { 2443 | // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. 2444 | Object.assign(value, value.description); 2445 | // Includes the id of the inventory item. 2446 | value['id'] = key; 2447 | value['assetid'] = key; 2448 | arr.push(value); 2449 | } 2450 | } 2451 | } 2452 | 2453 | // Some inventories (e.g. BattleBlock Theater) do not have child inventories, they have just one. 2454 | for (const key in getActiveInventory().m_rgAssets) { 2455 | const value = getActiveInventory().m_rgAssets[key]; 2456 | if (typeof value === 'object') { 2457 | // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. 2458 | Object.assign(value, value.description); 2459 | // Includes the id of the inventory item. 2460 | value['id'] = key; 2461 | value['assetid'] = key; 2462 | arr.push(value); 2463 | } 2464 | } 2465 | 2466 | return arr; 2467 | } 2468 | } 2469 | //#endregion 2470 | 2471 | //#region Inventory + Tradeoffer 2472 | if (currentPage == PAGE_INVENTORY || currentPage == PAGE_TRADEOFFER) { 2473 | 2474 | // Gets the active inventory. 2475 | function getActiveInventory() { 2476 | return unsafeWindow.g_ActiveInventory; 2477 | } 2478 | 2479 | // Sets the prices for the items. 2480 | function setInventoryPrices(items) { 2481 | inventoryPriceQueue.kill(); 2482 | 2483 | items.forEach((item) => { 2484 | if (!item.marketable) { 2485 | return; 2486 | } 2487 | 2488 | if (!$(item.element).is(':visible')) { 2489 | return; 2490 | } 2491 | 2492 | inventoryPriceQueue.push(item); 2493 | }); 2494 | } 2495 | 2496 | const inventoryPriceQueue = async.queue( 2497 | (item, next) => { 2498 | inventoryPriceQueueWorker( 2499 | item, 2500 | false, 2501 | (success, cached) => { 2502 | if (success) { 2503 | setTimeout(() => next(), cached ? 0 : getRandomInt(1000, 1500)); 2504 | } else { 2505 | if (!item.ignoreErrors) { 2506 | item.ignoreErrors = true; 2507 | inventoryPriceQueue.push(item); 2508 | } 2509 | 2510 | numberOfFailedRequests++; 2511 | 2512 | const delay = numberOfFailedRequests > 1 ? getRandomInt(30000, 45000) : getRandomInt(1000, 1500); 2513 | numberOfFailedRequests = numberOfFailedRequests > 3 ? 0 : numberOfFailedRequests; 2514 | 2515 | setTimeout(() => next(), cached ? 0 : delay); 2516 | } 2517 | } 2518 | ); 2519 | }, 2520 | 1 2521 | ); 2522 | 2523 | function inventoryPriceQueueWorker(item, ignoreErrors, callback) { 2524 | let failed = 0; 2525 | const itemName = item.name || item.description.name; 2526 | 2527 | // Only get the market orders here, the history is not important to visualize the current prices. 2528 | market.getItemOrdersHistogram( 2529 | item, 2530 | true, 2531 | (err, histogram, cachedListings) => { 2532 | if (err) { 2533 | logConsole(`无法获取 ${itemName} 的订单历史直方图`); 2534 | 2535 | if (err != ERROR_SUCCESS) { 2536 | failed += 1; 2537 | } 2538 | } 2539 | 2540 | if (failed > 0 && !ignoreErrors) { 2541 | return callback(false, cachedListings); 2542 | } 2543 | 2544 | const sellPrice = calculateSellPriceBeforeFees(null, histogram, false, 0, 65535); 2545 | 2546 | const itemPrice = sellPrice == 65535 2547 | ? '∞' 2548 | : formatPrice(market.getPriceIncludingFees(sellPrice)); 2549 | 2550 | const elementName = `${(currentPage == PAGE_TRADEOFFER ? '#item' : '#')}${item.appid}_${item.contextid}_${item.id}`; 2551 | const element = $(elementName); 2552 | 2553 | $('.inventory_item_price', element).remove(); 2554 | element.append(`${itemPrice}`); 2555 | 2556 | return callback(true, cachedListings); 2557 | } 2558 | ); 2559 | } 2560 | } 2561 | //#endregion 2562 | 2563 | //#region Market 2564 | if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) { 2565 | const marketListingsRelistedAssets = []; 2566 | let marketProgressBar; 2567 | 2568 | function increaseMarketProgressMax() { 2569 | let value = marketProgressBar.max; 2570 | 2571 | // Reset the progress bar if it already completed 2572 | if (marketProgressBar.value === value) { 2573 | marketProgressBar.value = 0; 2574 | value = 0; 2575 | } 2576 | 2577 | marketProgressBar.max = value + 1; 2578 | marketProgressBar.removeAttribute('hidden'); 2579 | } 2580 | 2581 | function increaseMarketProgress() { 2582 | marketProgressBar.value += 1; 2583 | 2584 | if (marketProgressBar.value === marketProgressBar.max) { 2585 | marketProgressBar.setAttribute('hidden', 'true'); 2586 | } 2587 | } 2588 | 2589 | // Match number part from any currency format 2590 | const getPriceValueAsInt = listing => 2591 | unsafeWindow.GetPriceValueAsInt( 2592 | listing.match(/(?[0-9][0-9 .,]*)/)?.groups?.price ?? 0 2593 | ); 2594 | 2595 | const marketListingsQueue = async.queue((listing, next) => { 2596 | marketListingsQueueWorker( 2597 | listing, 2598 | false, 2599 | (success, cached) => { 2600 | const callback = () => { 2601 | increaseMarketProgress(); 2602 | next(); 2603 | }; 2604 | 2605 | if (success) { 2606 | setTimeout(callback, cached ? 0 : getRandomInt(1000, 1500)); 2607 | } else { 2608 | setTimeout(() => marketListingsQueueWorker(listing, true, callback), cached ? 0 : getRandomInt(30000, 45000)); 2609 | } 2610 | } 2611 | ); 2612 | }, 1); 2613 | 2614 | 2615 | function marketListingsQueueWorker(listing, ignoreErrors, callback) { 2616 | const asset = unsafeWindow.g_rgAssets[listing.appid][listing.contextid][listing.assetid]; 2617 | 2618 | // An asset: 2619 | //{ 2620 | // "currency" : 0, 2621 | // "appid" : 753, 2622 | // "contextid" : "6", 2623 | // "id" : "4363079664", 2624 | // "classid" : "2228526061", 2625 | // "instanceid" : "0", 2626 | // "amount" : "1", 2627 | // "status" : 2, 2628 | // "original_amount" : "1", 2629 | // "background_color" : "", 2630 | // "icon_url" : "xx", 2631 | // "icon_url_large" : "xxx", 2632 | // "descriptions" : [{ 2633 | // "value" : "Their dense, shaggy fur conceals the presence of swams of moogamites, purple scaly skin, and more nipples than one would expect." 2634 | // } 2635 | // ], 2636 | // "tradable" : 1, 2637 | // "owner_actions" : [{ 2638 | // "link" : "http://steamcommunity.com/my/gamecards/443880/", 2639 | // "name" : "View badge progress" 2640 | // }, { 2641 | // "link" : "javascript:GetGooValue( '%contextid%', '%assetid%', 443880, 7, 0 )", 2642 | // "name" : "Turn into Gems..." 2643 | // } 2644 | // ], 2645 | // "name" : "Wook", 2646 | // "type" : "Loot Rascals Trading Card", 2647 | // "market_name" : "Wook", 2648 | // "market_hash_name" : "443880-Wook", 2649 | // "market_fee_app" : 443880, 2650 | // "commodity" : 1, 2651 | // "market_tradable_restriction" : 7, 2652 | // "market_marketable_restriction" : 7, 2653 | // "marketable" : 1, 2654 | // "app_icon" : "xxxx", 2655 | // "owner" : 0 2656 | //} 2657 | 2658 | const market_hash_name = getMarketHashName(asset); 2659 | const appid = listing.appid; 2660 | 2661 | const listingUI = $(getListingFromLists(listing.listingid).elm); 2662 | 2663 | const game_name = asset.type; 2664 | const price = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listingUI).text()); 2665 | 2666 | if (price <= getSettingWithDefault(SETTING_PRICE_MIN_CHECK_PRICE) * 100) { 2667 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_NOT_CHECKED); 2668 | $('.market_listing_my_price', listingUI).last().prop('title', 'The price is not checked.'); 2669 | listingUI.addClass('not_checked'); 2670 | 2671 | return callback(true, true); 2672 | } 2673 | 2674 | const priceInfo = getPriceInformationFromItem(asset); 2675 | const item = { 2676 | appid: parseInt(appid), 2677 | description: { 2678 | market_hash_name: market_hash_name 2679 | } 2680 | }; 2681 | 2682 | let failed = 0; 2683 | 2684 | market.getPriceHistory( 2685 | item, 2686 | true, 2687 | (errorPriceHistory, history, cachedHistory) => { 2688 | if (errorPriceHistory) { 2689 | logConsole(`无法获取 ${game_name} 的价格历史`); 2690 | 2691 | if (errorPriceHistory != ERROR_SUCCESS) { 2692 | failed += 1; 2693 | } 2694 | } 2695 | 2696 | market.getItemOrdersHistogram( 2697 | item, 2698 | true, 2699 | (errorHistogram, histogram, cachedListings) => { 2700 | if (errorHistogram) { 2701 | logConsole(`无法获取 ${game_name} 的订单历史直方图`); 2702 | 2703 | if (errorHistogram != ERROR_SUCCESS) { 2704 | failed += 1; 2705 | } 2706 | } 2707 | 2708 | if (failed > 0 && !ignoreErrors) { 2709 | return callback(false, cachedHistory && cachedListings); 2710 | } 2711 | 2712 | // Shows the highest buy order price on the market listings. 2713 | // The 'histogram.highest_buy_order' is not reliable as Steam is caching this value, but it gives some idea for older titles/listings. 2714 | const highestBuyOrderPrice = histogram == null || histogram.highest_buy_order == null 2715 | ? '-' 2716 | : formatPrice(histogram.highest_buy_order); 2717 | $( 2718 | '.market_table_value > span:nth-child(1) > span:nth-child(1) > span:nth-child(1)', 2719 | listingUI 2720 | ).append(` ➤ ${highestBuyOrderPrice 2721 | }`); 2722 | 2723 | logConsole('============================'); 2724 | logConsole(JSON.stringify(listing)); 2725 | logConsole(`${game_name}: ${asset.name}`); 2726 | logConsole(`当前价格:${price / 100.0}`); 2727 | 2728 | // Calculate two prices here, one without the offset and one with the offset. 2729 | // 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). 2730 | // The price with the offset should be used for relisting so it will still apply the user-set offset. 2731 | 2732 | const sellPriceWithoutOffset = calculateSellPriceBeforeFees( 2733 | history, 2734 | histogram, 2735 | false, 2736 | priceInfo.minPriceBeforeFees, 2737 | priceInfo.maxPriceBeforeFees 2738 | ); 2739 | const sellPriceWithOffset = calculateSellPriceBeforeFees( 2740 | history, 2741 | histogram, 2742 | true, 2743 | priceInfo.minPriceBeforeFees, 2744 | priceInfo.maxPriceBeforeFees 2745 | ); 2746 | 2747 | const sellPriceWithoutOffsetWithFees = market.getPriceIncludingFees(sellPriceWithoutOffset); 2748 | 2749 | logConsole(`计算出的价格:${sellPriceWithoutOffsetWithFees / 100.0} (${sellPriceWithoutOffset / 100.0})`); 2750 | 2751 | listingUI.addClass(`price_${sellPriceWithOffset}`); 2752 | 2753 | $('.market_listing_my_price', listingUI).last().prop( 2754 | 'title', 2755 | `最好的价格是 ${formatPrice(sellPriceWithoutOffsetWithFees)}。` 2756 | ); 2757 | 2758 | if (sellPriceWithoutOffsetWithFees < price) { 2759 | logConsole('售价太高。'); 2760 | 2761 | $('.market_listing_my_price', listingUI).last(). 2762 | css('background', COLOR_PRICE_EXPENSIVE); 2763 | listingUI.addClass('overpriced'); 2764 | 2765 | if (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1) { 2766 | queueOverpricedItemListing(listing.listingid); 2767 | } 2768 | } else if (sellPriceWithoutOffsetWithFees > price) { 2769 | logConsole('售价太低。'); 2770 | 2771 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_CHEAP); 2772 | listingUI.addClass('underpriced'); 2773 | } else { 2774 | logConsole('售价正好。'); 2775 | 2776 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_FAIR); 2777 | listingUI.addClass('fair'); 2778 | } 2779 | 2780 | return callback(true, cachedHistory && cachedListings); 2781 | } 2782 | ); 2783 | } 2784 | ); 2785 | } 2786 | 2787 | const marketOverpricedQueue = async.queue( 2788 | (item, next) => { 2789 | marketOverpricedQueueWorker( 2790 | item, 2791 | false, 2792 | (success) => { 2793 | const callback = () => { 2794 | increaseMarketProgress(); 2795 | next(); 2796 | }; 2797 | 2798 | if (success) { 2799 | setTimeout(callback, getRandomInt(1000, 1500)); 2800 | } else { 2801 | setTimeout(() => marketOverpricedQueueWorker(item, true, callback), getRandomInt(30000, 45000)); 2802 | } 2803 | } 2804 | ); 2805 | }, 2806 | 1 2807 | ); 2808 | 2809 | function marketOverpricedQueueWorker(item, ignoreErrors, callback) { 2810 | const listingUI = getListingFromLists(item.listing).elm; 2811 | 2812 | market.removeListing( 2813 | item.listing, false, 2814 | (errorRemove) => { 2815 | if (!errorRemove) { 2816 | $('.actual_content', listingUI).css('background', COLOR_PENDING); 2817 | 2818 | setTimeout(() => { 2819 | const itemName = $('.market_listing_item_name_link', listingUI).first().attr('href'); 2820 | const marketHashNameIndex = itemName.lastIndexOf('/') + 1; 2821 | const marketHashName = itemName.substring(marketHashNameIndex); 2822 | const decodedMarketHashName = decodeURIComponent(itemName.substring(marketHashNameIndex)); 2823 | let newAssetId = -1; 2824 | 2825 | unsafeWindow.RequestFullInventory(`${market.inventoryUrl + item.appid}/${item.contextid}/`, {}, null, null, (transport) => { 2826 | if (transport.responseJSON && transport.responseJSON.success) { 2827 | const inventory = transport.responseJSON.rgInventory; 2828 | 2829 | for (const child in inventory) { 2830 | if (marketListingsRelistedAssets.indexOf(child) == -1 && inventory[child].appid == item.appid && (inventory[child].market_hash_name == decodedMarketHashName || inventory[child].market_hash_name == marketHashName)) { 2831 | newAssetId = child; 2832 | break; 2833 | } 2834 | } 2835 | 2836 | if (newAssetId == -1) { 2837 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2838 | return callback(false); 2839 | } 2840 | 2841 | item.assetid = newAssetId; 2842 | marketListingsRelistedAssets.push(newAssetId); 2843 | 2844 | market.sellItem( 2845 | item, 2846 | item.sellPrice, 2847 | (errorSell) => { 2848 | if (!errorSell) { 2849 | $('.actual_content', listingUI).css('background', COLOR_SUCCESS); 2850 | 2851 | setTimeout(() => { 2852 | removeListingFromLists(item.listing); 2853 | }, 3000); 2854 | 2855 | return callback(true); 2856 | } else { 2857 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2858 | return callback(false); 2859 | } 2860 | } 2861 | ); 2862 | 2863 | } else { 2864 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2865 | return callback(false); 2866 | } 2867 | }); 2868 | }, getRandomInt(1500, 2500)); // Wait a little to make sure the item is returned to inventory. 2869 | } else { 2870 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2871 | return callback(false); 2872 | } 2873 | } 2874 | ); 2875 | } 2876 | 2877 | // Queue an overpriced item listing to be relisted. 2878 | function queueOverpricedItemListing(listingid) { 2879 | const assetInfo = getAssetInfoFromListingId(listingid); 2880 | const listingUI = $(getListingFromLists(listingid).elm); 2881 | let price = -1; 2882 | 2883 | const items = $(listingUI).attr('class').split(' '); 2884 | for (const i in items) { 2885 | if (items[i].toString().includes('price_')) { 2886 | price = parseInt(items[i].toString().replace('price_', '')); 2887 | } 2888 | } 2889 | 2890 | if (price > 0) { 2891 | marketOverpricedQueue.push({ 2892 | listing: listingid, 2893 | assetid: assetInfo.assetid, 2894 | contextid: assetInfo.contextid, 2895 | appid: assetInfo.appid, 2896 | sellPrice: price 2897 | }); 2898 | increaseMarketProgressMax(); 2899 | } 2900 | } 2901 | 2902 | const marketRemoveQueue = async.queue( 2903 | (listingid, next) => { 2904 | marketRemoveQueueWorker( 2905 | listingid, 2906 | false, 2907 | (success) => { 2908 | const callback = () => { 2909 | increaseMarketProgress(); 2910 | next(); 2911 | }; 2912 | 2913 | if (success) { 2914 | setTimeout(callback, getRandomInt(50, 100)); 2915 | } else { 2916 | setTimeout(() => marketRemoveQueueWorker(listingid, true, callback), getRandomInt(30000, 45000)); 2917 | } 2918 | } 2919 | ); 2920 | }, 2921 | 1 2922 | ); 2923 | 2924 | function marketRemoveQueueWorker(listingid, ignoreErrors, callback) { 2925 | const listingUI = getListingFromLists(listingid).elm; 2926 | const isBuyOrder = listingUI.id.startsWith('mybuyorder_'); 2927 | 2928 | market.removeListing( 2929 | listingid, isBuyOrder, 2930 | (errorRemove) => { 2931 | if (!errorRemove) { 2932 | $('.actual_content', listingUI).css('background', COLOR_SUCCESS); 2933 | 2934 | setTimeout( 2935 | () => { 2936 | removeListingFromLists(listingid); 2937 | 2938 | const numberOfListings = marketLists[0].size; 2939 | if (numberOfListings > 0) { 2940 | $('#my_market_selllistings_number').text(numberOfListings.toString()); 2941 | 2942 | // This seems identical to the number of sell listings. 2943 | $('#my_market_activelistings_number').text(numberOfListings.toString()); 2944 | } 2945 | }, 2946 | 3000 2947 | ); 2948 | 2949 | return callback(true); 2950 | } else { 2951 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2952 | 2953 | return callback(false); 2954 | } 2955 | } 2956 | ); 2957 | } 2958 | 2959 | const marketListingsItemsQueue = async.queue( 2960 | (listing, next) => { 2961 | const callback = () => { 2962 | increaseMarketProgress(); 2963 | setTimeout(() => next(), getRandomInt(1000, 1500)); 2964 | }; 2965 | 2966 | const url = `${window.location.origin}/market/mylistings` 2967 | 2968 | const options = { 2969 | method: 'GET', 2970 | data: { 2971 | count: 100, 2972 | start: listing 2973 | }, 2974 | responseType: 'json' 2975 | }; 2976 | 2977 | request( 2978 | url, 2979 | options, 2980 | (error, data) => { 2981 | if (error || !data?.success) { 2982 | callback(); 2983 | return; 2984 | } 2985 | 2986 | const myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); 2987 | 2988 | const nodes = $.parseHTML(data.results_html); 2989 | const rows = $('.market_listing_row', nodes); 2990 | myMarketListings.append(rows); 2991 | 2992 | // g_rgAssets 2993 | unsafeWindow.MergeWithAssetArray(data.assets); // This is a method from Steam. 2994 | 2995 | callback(); 2996 | } 2997 | ) 2998 | }, 2999 | 1 3000 | ); 3001 | 3002 | marketListingsItemsQueue.drain(() => { 3003 | const myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); 3004 | myMarketListings.checkboxes('range', true); 3005 | 3006 | // Sometimes the Steam API is returning duplicate entries (especially during item listing), filter these. 3007 | const seen = {}; 3008 | $('.market_listing_row', myMarketListings).each(function () { 3009 | const item_id = $(this).attr('id'); 3010 | if (seen[item_id]) { 3011 | $(this).remove(); 3012 | } else { 3013 | seen[item_id] = true; 3014 | } 3015 | 3016 | // Remove listings awaiting confirmations, they are already listed separately. 3017 | if ($('.item_market_action_button', this).attr('href').toLowerCase(). 3018 | includes('CancelMarketListingConfirmation'.toLowerCase())) { 3019 | $(this).remove(); 3020 | } 3021 | 3022 | // Remove buy order listings, they are already listed separately. 3023 | if ($('.item_market_action_button', this).attr('href').toLowerCase(). 3024 | includes('CancelMarketBuyOrder'.toLowerCase())) { 3025 | $(this).remove(); 3026 | } 3027 | }); 3028 | 3029 | // Now add the market checkboxes. 3030 | addMarketCheckboxes(); 3031 | 3032 | // Show the listings again, rendering is done. 3033 | $('#market_listings_spinner').remove(); 3034 | myMarketListings.show(); 3035 | 3036 | fillMarketListingsQueue(); 3037 | }); 3038 | 3039 | function fillMarketListingsQueue() { 3040 | $('.market_home_listing_table').each(function (e) { 3041 | 3042 | // Not for popular / new / recently sold items (bottom of page). 3043 | if ($('.my_market_header', $(this)).length == 0) { 3044 | return; 3045 | } 3046 | 3047 | // Buy orders and listings confirmations are not grouped like the sell listings, add this so pagination works there as well. 3048 | if (!$(this).attr('id')) { 3049 | $(this).attr('id', `market-listing-${e}`); 3050 | 3051 | $(this).append(`
`); 3052 | $('.market_listing_row', $(this)).appendTo($(`#market-listing-container-${e}`)); 3053 | } else { 3054 | $(this).children().last().addClass('market_listing_see'); 3055 | } 3056 | 3057 | const marketListing = $('.market_listing_see', this).last(); 3058 | if (marketListing[0].childElementCount > 0) { 3059 | addMarketListings(marketListing); 3060 | sortMarketListings($(this), false, false, true); 3061 | } 3062 | }); 3063 | 3064 | let totalPriceBuyer = 0; 3065 | let totalPriceSeller = 0; 3066 | let totalAmount = 0; 3067 | 3068 | // Add the listings to the queue to be checked for the price. 3069 | marketLists.flatMap(list => list.items).forEach(item => { 3070 | const listingid = replaceNonNumbers(item.values().market_listing_item_name); 3071 | const assetInfo = getAssetInfoFromListingId(listingid); 3072 | 3073 | if (assetInfo.appid === undefined) { 3074 | logConsole(`Skipping listing ${listingid} (no sell order)`); 3075 | return; 3076 | } 3077 | 3078 | totalAmount += assetInfo.amount; 3079 | 3080 | if (!isNaN(assetInfo.priceBuyer)) { 3081 | totalPriceBuyer += assetInfo.priceBuyer * assetInfo.amount; 3082 | } 3083 | if (!isNaN(assetInfo.priceSeller)) { 3084 | totalPriceSeller += assetInfo.priceSeller * assetInfo.amount; 3085 | } 3086 | 3087 | marketListingsQueue.push({ 3088 | listingid, 3089 | appid: assetInfo.appid, 3090 | contextid: assetInfo.contextid, 3091 | assetid: assetInfo.assetid 3092 | }); 3093 | increaseMarketProgressMax(); 3094 | }); 3095 | 3096 | $('#my_market_selllistings_number').append(` [${totalAmount}]`) 3097 | .append(`, ${formatPrice(totalPriceBuyer)} ➤ ${formatPrice(totalPriceSeller)}`); 3098 | } 3099 | 3100 | 3101 | // Gets the asset info (appid/contextid/assetid) based on a listingid. 3102 | function getAssetInfoFromListingId(listingid) { 3103 | const listing = getListingFromLists(listingid); 3104 | if (listing == null) { 3105 | return {}; 3106 | } 3107 | 3108 | const actionButton = $('.item_market_action_button', listing.elm).attr('href'); 3109 | // Market buy orders have no asset info. 3110 | if (actionButton == null || actionButton.toLowerCase().includes('cancelmarketbuyorder')) { 3111 | return {}; 3112 | } 3113 | 3114 | const priceBuyer = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listing.elm).text()); 3115 | const priceSeller = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(3)', listing.elm).text()); 3116 | const itemIds = actionButton.split(','); 3117 | const appid = replaceNonNumbers(itemIds[2]); 3118 | const contextid = replaceNonNumbers(itemIds[3]); 3119 | const assetid = replaceNonNumbers(itemIds[4]); 3120 | const amount = Number(unsafeWindow.g_rgAssets[appid][contextid][assetid]?.amount ?? 1); 3121 | return { 3122 | appid, 3123 | contextid, 3124 | assetid, 3125 | amount, 3126 | priceBuyer, 3127 | priceSeller 3128 | }; 3129 | } 3130 | 3131 | // Adds market item listings. 3132 | function addMarketListings(market_listing_see) { 3133 | market_listing_see.addClass('list'); 3134 | 3135 | $('.market_listing_table_header', market_listing_see.parent()). 3136 | append(''); 3137 | 3138 | const options = { 3139 | valueNames: [ 3140 | 'market_listing_game_name', 3141 | 'market_listing_item_name_link', 3142 | 'market_listing_price', 3143 | 'market_listing_listed_date', 3144 | { 3145 | name: 'market_listing_item_name', 3146 | attr: 'id' 3147 | } 3148 | ] 3149 | }; 3150 | 3151 | try { 3152 | const list = new List(market_listing_see.parent().get(0), options); 3153 | list.on('searchComplete', updateMarketSelectAllButton); 3154 | marketLists.push(list); 3155 | } catch (e) { 3156 | console.error(e); 3157 | } 3158 | } 3159 | 3160 | // Adds checkboxes to market listings. 3161 | function addMarketCheckboxes() { 3162 | $('.market_listing_row').each(function () { 3163 | // Don't add it again, one time is enough. 3164 | if ($('.market_listing_select', this).length == 0) { 3165 | $('.market_listing_cancel_button', $(this)).append('
' + 3166 | '' + 3167 | '
'); 3168 | 3169 | $('.market_select_item', this).change(() => { 3170 | updateMarketSelectAllButton(); 3171 | }); 3172 | } 3173 | }); 3174 | } 3175 | 3176 | // Process the market listings. 3177 | function processMarketListings() { 3178 | addMarketCheckboxes(); 3179 | 3180 | if (currentPage == PAGE_MARKET) { 3181 | // Load the market listings. 3182 | let currentCount = 0; 3183 | let totalCount = 0; 3184 | 3185 | if (typeof unsafeWindow.g_oMyListings !== 'undefined' && unsafeWindow.g_oMyListings != null && unsafeWindow.g_oMyListings.m_cTotalCount != null) { 3186 | totalCount = unsafeWindow.g_oMyListings.m_cTotalCount; 3187 | } else { 3188 | totalCount = parseInt($('#my_market_selllistings_number').text()); 3189 | } 3190 | 3191 | if (isNaN(totalCount) || totalCount == 0) { 3192 | fillMarketListingsQueue(); 3193 | return; 3194 | } 3195 | 3196 | $('#tabContentsMyActiveMarketListingsRows').html(''); // Clear the default listings. 3197 | $('#tabContentsMyActiveMarketListingsRows').hide(); // Hide all listings until everything has been loaded. 3198 | 3199 | // Hide Steam's paging controls. 3200 | $('#tabContentsMyActiveMarketListings_ctn').hide(); 3201 | $('.market_pagesize_options').hide(); 3202 | 3203 | // Show the spinner so the user knows that something is going on. 3204 | $('.my_market_header').eq(0).append(`
${spinnerBlock 3205 | }
正在加载交易列表
` + 3206 | '
'); 3207 | 3208 | while (currentCount < totalCount) { 3209 | marketListingsItemsQueue.push(currentCount); 3210 | increaseMarketProgressMax(); 3211 | currentCount += 100; 3212 | } 3213 | } else { 3214 | // This is on a market item page. 3215 | $('.market_home_listing_table').each(function () { 3216 | // Not on 'x requests to buy at y,yy or lower'. 3217 | if ($('#market_buyorder_info_show_details', $(this)).length > 0) { 3218 | return; 3219 | } 3220 | 3221 | $(this).children().last().wrap('
'); 3222 | const marketListing = $('.market_listing_see', this).last(); 3223 | 3224 | if (marketListing[0].childElementCount > 0) { 3225 | addMarketListings(marketListing); 3226 | sortMarketListings($(this), false, false, true); 3227 | } 3228 | }); 3229 | 3230 | $('#tabContentsMyActiveMarketListingsRows > .market_listing_row').each(function () { 3231 | const listingid = $(this).attr('id').replace('mylisting_', '').replace('mybuyorder_', '').replace('mbuyorder_', ''); 3232 | const assetInfo = getAssetInfoFromListingId(listingid); 3233 | 3234 | // There's only one item in the g_rgAssets on a market listing page. 3235 | let existingAsset = null; 3236 | for (const appid in unsafeWindow.g_rgAssets) { 3237 | for (const contextid in unsafeWindow.g_rgAssets[appid]) { 3238 | for (const assetid in unsafeWindow.g_rgAssets[appid][contextid]) { 3239 | existingAsset = unsafeWindow.g_rgAssets[appid][contextid][assetid]; 3240 | break; 3241 | } 3242 | } 3243 | } 3244 | 3245 | // appid and contextid are identical, only the assetid is different for each asset. 3246 | unsafeWindow.g_rgAssets[assetInfo.appid][assetInfo.contextid][assetInfo.assetid] = existingAsset; 3247 | marketListingsQueue.push({ 3248 | listingid, 3249 | appid: assetInfo.appid, 3250 | contextid: assetInfo.contextid, 3251 | assetid: assetInfo.assetid 3252 | }); 3253 | increaseMarketProgressMax(); 3254 | }); 3255 | } 3256 | } 3257 | 3258 | // Update the select/deselect all button on the market. 3259 | function updateMarketSelectAllButton() { 3260 | $('.market_listing_buttons').each(function () { 3261 | const selectionGroup = $(this).parent().parent(); 3262 | let invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length; 3263 | if ($('.market_select_item', selectionGroup).length == 0) { // If there are no items to select, keep it at Select all. 3264 | invert = false; 3265 | } 3266 | $('.select_all > span', selectionGroup).text(invert ? '取消所选物品' : '选中全部物品'); 3267 | }); 3268 | } 3269 | 3270 | // Sort the market listings. 3271 | function sortMarketListings(elem, isPrice, isDateOrQuantity, isName) { 3272 | const list = getListFromContainer(elem); 3273 | if (list == null) { 3274 | console.log('无效参数,找不到匹配元素的列表。'); 3275 | return; 3276 | } 3277 | 3278 | // Change sort order (asc/desc). 3279 | let asc = true; 3280 | 3281 | // (Re)set the asc/desc arrows. 3282 | const arrow_down = '🡻'; 3283 | const arrow_up = '🡹'; 3284 | 3285 | $('.market_listing_table_header > span', elem).each(function () { 3286 | if ($(this).hasClass('market_listing_edit_buttons')) { 3287 | return; 3288 | } 3289 | 3290 | if ($(this).text().includes(arrow_up)) { 3291 | asc = false; 3292 | } 3293 | 3294 | $(this).text($(this).text().replace(` ${arrow_down}`, '').replace(` ${arrow_up}`, '')); 3295 | }); 3296 | 3297 | let market_listing_selector; 3298 | if (isPrice) { 3299 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(1); 3300 | } else if (isDateOrQuantity) { 3301 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(2); 3302 | } else if (isName) { 3303 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(3); 3304 | } 3305 | market_listing_selector.text(`${market_listing_selector.text()} ${asc ? arrow_up : arrow_down}`); 3306 | 3307 | if (list.sort == null) { 3308 | return; 3309 | } 3310 | 3311 | const isBuyOrder = list.list.querySelectorAll('.market_listing_buyorder_qty').length >= 1; 3312 | 3313 | if (isName) { 3314 | list.sort('', { 3315 | order: asc ? 'asc' : 'desc', 3316 | sortFunction: function (a, b) { 3317 | if (a.values().market_listing_game_name.toLowerCase(). 3318 | localeCompare(b.values().market_listing_game_name.toLowerCase()) == 3319 | 0) { 3320 | return a.values().market_listing_item_name_link.toLowerCase(). 3321 | localeCompare(b.values().market_listing_item_name_link.toLowerCase()); 3322 | } 3323 | return a.values().market_listing_game_name.toLowerCase(). 3324 | localeCompare(b.values().market_listing_game_name.toLowerCase()); 3325 | } 3326 | }); 3327 | } else if (isDateOrQuantity) { 3328 | const currentMonth = luxon.DateTime.local().month; 3329 | 3330 | if (isBuyOrder) { 3331 | list.sort('market_listing_buyorder_qty', { 3332 | order: asc ? 'asc' : 'desc', 3333 | sortFunction: function (a, b) { 3334 | const quantityA = a.elm.querySelector('.market_listing_buyorder_qty').innerText; 3335 | const quantityB = b.elm.querySelector('.market_listing_buyorder_qty').innerText; 3336 | 3337 | return quantityA - quantityB; 3338 | } 3339 | }); 3340 | } else { 3341 | list.sort('market_listing_listed_date', { 3342 | order: asc ? 'asc' : 'desc', 3343 | sortFunction: function (a, b) { 3344 | let firstDate = luxon.DateTime.fromString(a.values().market_listing_listed_date.trim(), 'd MMM'); 3345 | let secondDate = luxon.DateTime.fromString(b.values().market_listing_listed_date.trim(), 'd MMM'); 3346 | 3347 | if (firstDate == null || secondDate == null) { 3348 | return 0; 3349 | } 3350 | 3351 | if (firstDate.month > currentMonth) { 3352 | firstDate = firstDate.plus({ years: -1 }); 3353 | } 3354 | if (secondDate.month > currentMonth) { 3355 | secondDate = secondDate.plus({ years: -1 }); 3356 | } 3357 | 3358 | if (firstDate > secondDate) { 3359 | return 1; 3360 | } 3361 | if (firstDate === secondDate) { 3362 | return 0; 3363 | } 3364 | return -1; 3365 | } 3366 | }); 3367 | } 3368 | } else if (isPrice) { 3369 | list.sort('market_listing_price', { 3370 | order: asc ? 'asc' : 'desc', 3371 | sortFunction: function (a, b) { 3372 | if (!isBuyOrder) { 3373 | let listingPriceA = $(a.values().market_listing_price).text(); 3374 | listingPriceA = listingPriceA.substr(0, listingPriceA.indexOf('(')); 3375 | 3376 | let listingPriceB = $(b.values().market_listing_price).text(); 3377 | listingPriceB = listingPriceB.substr(0, listingPriceB.indexOf('(')); 3378 | 3379 | const firstPrice = getPriceValueAsInt(listingPriceA); 3380 | const secondPrice = getPriceValueAsInt(listingPriceB); 3381 | 3382 | return firstPrice - secondPrice; 3383 | } else { 3384 | const priceA = getPriceValueAsInt(a.elm.querySelector('div:nth-child(3) > span:nth-child(1) > span:nth-child(1)').innerText); 3385 | const priceB = getPriceValueAsInt(b.elm.querySelector('div:nth-child(3) > span:nth-child(1) > span:nth-child(1)').innerText); 3386 | 3387 | return priceA - priceB; 3388 | } 3389 | } 3390 | }); 3391 | } 3392 | } 3393 | 3394 | function getListFromContainer(group) { 3395 | for (let i = 0; i < marketLists.length; i++) { 3396 | if (group.attr('id') == $(marketLists[i].listContainer).attr('id')) { 3397 | return marketLists[i]; 3398 | } 3399 | } 3400 | } 3401 | 3402 | function getListingFromLists(listingid) { 3403 | // Sometimes listing ids are contained in multiple lists (?), use the last one available as this is the one we're most likely interested in. 3404 | for (let i = marketLists.length - 1; i >= 0; i--) { 3405 | let values = marketLists[i].get('market_listing_item_name', `mylisting_${listingid}_name`); 3406 | if (values != null && values.length > 0) { 3407 | return values[0]; 3408 | } 3409 | 3410 | values = marketLists[i].get('market_listing_item_name', `mbuyorder_${listingid}_name`); 3411 | if (values != null && values.length > 0) { 3412 | return values[0]; 3413 | } 3414 | } 3415 | 3416 | 3417 | } 3418 | 3419 | function removeListingFromLists(listingid) { 3420 | for (let i = 0; i < marketLists.length; i++) { 3421 | marketLists[i].remove('market_listing_item_name', `mylisting_${listingid}_name`); 3422 | marketLists[i].remove('market_listing_item_name', `mbuyorder_${listingid}_name`); 3423 | } 3424 | } 3425 | 3426 | // Initialize the market UI. 3427 | function initializeMarketUI() { 3428 | $('.market_header_text').append('