├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── feature.yml └── workflows │ └── lint.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── icon.svg ├── inventory.png ├── market.png ├── settings.png └── tradeoffer.png ├── code.user.js ├── eslint.config.mjs └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://steamcommunity.com/id/nuklon/'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Describe the bug 8 | description: A clear and concise description of what the bug is and what page it happens on. Provide a screenshot and console logs if applicable. 9 | validations: 10 | required: true 11 | 12 | - type: input 13 | attributes: 14 | label: URL 15 | description: "The complete URL where the problem was found. I.e: https://steamcommunity.com/market/" 16 | validations: 17 | required: true 18 | 19 | - type: input 20 | attributes: 21 | label: Browser name and version 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea 3 | labels: [enhancement] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Is your feature request related to a problem? Please describe. 8 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like 15 | description: A clear and concise description of what you want to happen. 16 | 17 | - type: textarea 18 | attributes: 19 | label: Describe alternatives you've considered 20 | description: A clear and concise description of any alternative solutions or features you've considered. 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | name: CI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | 18 | - name: Install 19 | run: npm install 20 | 21 | - name: Test 22 | run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /package-lock.json 3 | -------------------------------------------------------------------------------- /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](assets/market.png) 47 | 48 | 49 | *Inventory* 50 | 51 | ![Inventory](assets/inventory.png) 52 | 53 | 54 | *Options* 55 | 56 | ![Options](assets/settings.png) 57 | 58 | 59 | *Trade offers* 60 | 61 | ![Tradeoffers](assets/tradeoffer.png) 62 | 63 | 64 | ### License 65 | 66 | [MIT](LICENSE) 67 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/inventory.png -------------------------------------------------------------------------------- /assets/market.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/market.png -------------------------------------------------------------------------------- /assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/settings.png -------------------------------------------------------------------------------- /assets/tradeoffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/tradeoffer.png -------------------------------------------------------------------------------- /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 Enhances the Steam Inventory and Steam Market. 9 | // @match https://steamcommunity.com/id/*/inventory* 10 | // @match https://steamcommunity.com/profiles/*/inventory* 11 | // @match https://steamcommunity.com/market* 12 | // @match https://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://github.com/Nuklon/Steam-Economy-Enhancer 23 | // @homepage https://github.com/Nuklon/Steam-Economy-Enhancer 24 | // @supportURL https://github.com/Nuklon/Steam-Economy-Enhancer/issues 25 | // @downloadURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js 26 | // @updateURL https://raw.githubusercontent.com/Nuklon/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 error'); 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('Request failed'); 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(`Failed to get local storage item ${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(`Failed to set local storage item ${name}, ${e}.`); 286 | return false; 287 | } 288 | } 289 | 290 | function getSessionStorageItem(name) { 291 | try { 292 | return sessionStorage.getItem(name); 293 | } catch (e) { 294 | logConsole(`Failed to get session storage item ${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(`Failed to set session storage item ${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 | const rgMatches = action.link.match(/GetGooValue\( *'%contextid%', *'%assetid%', *'?(?[0-9]+)'?/); 605 | 606 | if (!rgMatches) { 607 | continue; 608 | } 609 | 610 | appid = rgMatches.groups.appid; 611 | break; 612 | } 613 | 614 | const url = `${this.inventoryUrlBase}ajaxgetgoovalue/`; 615 | 616 | const options = { 617 | method: 'GET', 618 | data: { 619 | sessionid: readCookie('sessionid'), 620 | appid: appid, 621 | assetid: item.assetid, 622 | contextid: item.contextid 623 | }, 624 | responseType: 'json' 625 | }; 626 | 627 | request( 628 | url, 629 | options, 630 | (error, data) => { 631 | if (error) { 632 | callback(ERROR_FAILED, data); 633 | return; 634 | } 635 | 636 | callback(ERROR_SUCCESS, data); 637 | } 638 | ); 639 | } catch { 640 | return callback(ERROR_FAILED); 641 | } 642 | //http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0 643 | // OR 644 | //http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6 645 | //sessionid=xyz 646 | //appid = 535690 647 | //assetid = 4830605461 648 | //contextid = 6 649 | }; 650 | 651 | 652 | // Grinds the item into gems. 653 | SteamMarket.prototype.grindIntoGoo = function (item, callback) { 654 | try { 655 | const url = `${this.inventoryUrlBase}ajaxgrindintogoo/`; 656 | 657 | const options = { 658 | method: 'POST', 659 | data: { 660 | sessionid: readCookie('sessionid'), 661 | appid: item.market_fee_app, 662 | assetid: item.assetid, 663 | contextid: item.contextid, 664 | goo_value_expected: item.goo_value_expected 665 | }, 666 | responseType: 'json' 667 | }; 668 | 669 | request( 670 | url, 671 | options, 672 | (error, data) => { 673 | if (error) { 674 | callback(ERROR_FAILED, data); 675 | return; 676 | } 677 | 678 | callback(ERROR_SUCCESS, data); 679 | } 680 | ); 681 | } catch { 682 | return callback(ERROR_FAILED); 683 | } 684 | 685 | //sessionid = xyz 686 | //appid = 535690 687 | //assetid = 4830605461 688 | //contextid = 6 689 | //goo_value_expected = 10 690 | //http://steamcommunity.com/my/ajaxgrindintogoo/ 691 | }; 692 | 693 | 694 | // Unpacks the booster pack. 695 | SteamMarket.prototype.unpackBoosterPack = function (item, callback) { 696 | try { 697 | const url = `${this.inventoryUrlBase}ajaxunpackbooster/`; 698 | 699 | const options = { 700 | method: 'POST', 701 | data: { 702 | sessionid: readCookie('sessionid'), 703 | appid: item.market_fee_app, 704 | communityitemid: item.assetid 705 | }, 706 | responseType: 'json' 707 | }; 708 | 709 | request( 710 | url, 711 | options, 712 | (error, data) => { 713 | if (error) { 714 | callback(ERROR_FAILED, data); 715 | return; 716 | } 717 | 718 | callback(ERROR_SUCCESS, data); 719 | } 720 | ); 721 | } catch { 722 | return callback(ERROR_FAILED); 723 | } 724 | 725 | //sessionid = xyz 726 | //appid = 535690 727 | //communityitemid = 4830605461 728 | //http://steamcommunity.com/my/ajaxunpackbooster/ 729 | }; 730 | 731 | // Get the current price history for an item. 732 | SteamMarket.prototype.getCurrentPriceHistory = function (appid, market_name, callback) { 733 | const url = `${window.location.origin}/market/pricehistory/`; 734 | 735 | const options = { 736 | method: 'GET', 737 | data: { 738 | appid: appid, 739 | market_hash_name: market_name 740 | }, 741 | responseType: 'json' 742 | }; 743 | 744 | request( 745 | url, 746 | options, 747 | (error, data) => { 748 | if (error) { 749 | callback(ERROR_FAILED); 750 | return; 751 | } 752 | 753 | if (data && (!data.success || !data.prices)) { 754 | callback(ERROR_DATA); 755 | return; 756 | } 757 | 758 | // Multiply prices so they're in pennies. 759 | for (let i = 0; i < data.prices.length; i++) { 760 | data.prices[i][1] *= 100; 761 | data.prices[i][2] = parseInt(data.prices[i][2]); 762 | } 763 | 764 | // Store the price history in the session storage. 765 | const storage_hash = `pricehistory_${appid}+${market_name}`; 766 | storageSession.setItem(storage_hash, data.prices); 767 | 768 | callback(ERROR_SUCCESS, data.prices, false); 769 | } 770 | ); 771 | }; 772 | 773 | // Get the item name id from a market item. 774 | // 775 | // This id never changes so we can store this in the persistent storage. 776 | SteamMarket.prototype.getMarketItemNameId = function (item, callback) { 777 | try { 778 | const market_name = getMarketHashName(item); 779 | if (market_name == null) { 780 | callback(ERROR_FAILED); 781 | return; 782 | } 783 | 784 | const appid = item.appid; 785 | const storage_hash = `itemnameid_${appid}+${market_name}`; 786 | 787 | storagePersistent.getItem(storage_hash). 788 | then((value) => { 789 | if (value != null) { 790 | callback(ERROR_SUCCESS, value); 791 | } else { 792 | return market.getCurrentMarketItemNameId(appid, market_name, callback); 793 | } 794 | }). 795 | catch(() => { 796 | return market.getCurrentMarketItemNameId(appid, market_name, callback); 797 | }); 798 | } catch { 799 | return callback(ERROR_FAILED); 800 | } 801 | }; 802 | 803 | // Get the item name id from a market item. 804 | SteamMarket.prototype.getCurrentMarketItemNameId = function (appid, market_name, callback) { 805 | const url = `${window.location.origin}/market/listings/${appid}/${escapeURI(market_name)}`; 806 | 807 | const options = { method: 'GET' }; 808 | 809 | request( 810 | url, 811 | options, 812 | (error, data) => { 813 | if (error) { 814 | callback(ERROR_FAILED); 815 | return; 816 | } 817 | 818 | const matches = (/Market_LoadOrderSpread\( (\d+) \);/).exec(data || ''); 819 | if (matches == null) { 820 | callback(ERROR_DATA); 821 | return; 822 | } 823 | 824 | const item_nameid = matches[1]; 825 | 826 | // Store the item name id in the persistent storage. 827 | const storage_hash = `itemnameid_${appid}+${market_name}`; 828 | storagePersistent.setItem(storage_hash, item_nameid); 829 | 830 | callback(ERROR_SUCCESS, item_nameid); 831 | } 832 | ); 833 | }; 834 | 835 | // Get the sales listings for this item in the market, with more information. 836 | // 837 | //{ 838 | //"success" : 1, 839 | //"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>", 840 | //"sell_order_summary" : "1965<\/span> for sale starting at 0,04\u20ac<\/span>", 841 | //"buy_order_table" : "
Price<\/th>Quantity<\/th><\/tr>
0,03\u20ac<\/td>93<\/td><\/tr><\/table>", 842 | //"buy_order_summary" : "93<\/span> requests to buy at 0,03\u20ac<\/span> or lower", 843 | //"highest_buy_order" : "3", 844 | //"lowest_sell_order" : "4", 845 | //"buy_order_graph" : [[0.03, 93, "93 buy orders at 0,03\u20ac or higher"]], 846 | //"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"]], 847 | //"graph_max_y" : 3000, 848 | //"graph_min_x" : 0.03, 849 | //"graph_max_x" : 0.14, 850 | //"price_prefix" : "", 851 | //"price_suffix" : "\u20ac" 852 | //} 853 | SteamMarket.prototype.getItemOrdersHistogram = function (item, cache, callback) { 854 | try { 855 | const market_name = getMarketHashName(item); 856 | if (market_name == null) { 857 | callback(ERROR_FAILED); 858 | return; 859 | } 860 | 861 | const appid = item.appid; 862 | 863 | if (cache) { 864 | const storage_hash = `itemordershistogram_${appid}+${market_name}`; 865 | storageSession.getItem(storage_hash). 866 | then((value) => { 867 | if (value != null) { 868 | callback(ERROR_SUCCESS, value, true); 869 | } else { 870 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 871 | } 872 | }). 873 | catch(() => { 874 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 875 | }); 876 | } else { 877 | market.getCurrentItemOrdersHistogram(item, market_name, callback); 878 | } 879 | 880 | } catch { 881 | return callback(ERROR_FAILED); 882 | } 883 | }; 884 | 885 | // Get the sales listings for this item in the market, with more information. 886 | SteamMarket.prototype.getCurrentItemOrdersHistogram = function (item, market_name, callback) { 887 | market.getMarketItemNameId( 888 | item, 889 | (error, item_nameid) => { 890 | if (error) { 891 | callback(ERROR_FAILED); 892 | return; 893 | } 894 | 895 | const url = `${window.location.origin}/market/itemordershistogram`; 896 | 897 | const options = { 898 | method: 'GET', 899 | data: { 900 | country: country, 901 | language: 'english', 902 | currency: currencyId, 903 | item_nameid: item_nameid, 904 | two_factor: 0 905 | } 906 | }; 907 | 908 | request( 909 | url, 910 | options, 911 | (error, data) => { 912 | if (error) { 913 | callback(ERROR_FAILED, null); 914 | return; 915 | } 916 | 917 | // Store the histogram in the session storage. 918 | const storage_hash = `itemordershistogram_${item.appid}+${market_name}`; 919 | storageSession.setItem(storage_hash, data); 920 | 921 | callback(ERROR_SUCCESS, data, false); 922 | } 923 | ) 924 | } 925 | ); 926 | }; 927 | 928 | // Calculate the price before fees (seller price) from the buyer price 929 | SteamMarket.prototype.getPriceBeforeFees = function (price, item) { 930 | let publisherFee = -1; 931 | 932 | if (item != null) { 933 | if (item.market_fee != null) { 934 | publisherFee = item.market_fee; 935 | } else if (item.description != null && item.description.market_fee != null) { 936 | publisherFee = item.description.market_fee; 937 | } 938 | } 939 | 940 | if (publisherFee == -1) { 941 | if (this.walletInfo != null) { 942 | publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; 943 | } else { 944 | publisherFee = 0.10; 945 | } 946 | } 947 | 948 | price = Math.round(price); 949 | const feeInfo = CalculateFeeAmount(price, publisherFee, this.walletInfo); 950 | return price - feeInfo.fees; 951 | }; 952 | 953 | // Calculate the buyer price from the seller price 954 | SteamMarket.prototype.getPriceIncludingFees = function (price, item) { 955 | let publisherFee = -1; 956 | if (item != null) { 957 | if (item.market_fee != null) { 958 | publisherFee = item.market_fee; 959 | } else if (item.description != null && item.description.market_fee != null) { 960 | publisherFee = item.description.market_fee; 961 | } 962 | } 963 | if (publisherFee == -1) { 964 | if (this.walletInfo != null) { 965 | publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; 966 | } else { 967 | publisherFee = 0.10; 968 | } 969 | } 970 | 971 | price = Math.round(price); 972 | const feeInfo = CalculateAmountToSendForDesiredReceivedAmount(price, publisherFee, this.walletInfo); 973 | return feeInfo.amount; 974 | }; 975 | //#endregion 976 | 977 | // Cannot use encodeURI / encodeURIComponent, Steam only escapes certain characters. 978 | function escapeURI(name) { 979 | let previousName = ''; 980 | while (previousName != name) { 981 | previousName = name; 982 | name = name.replace('?', '%3F'). 983 | replace('#', '%23'). 984 | replace(' ', '%09'); 985 | } 986 | return name; 987 | } 988 | 989 | //#region Steam Market / Inventory helpers 990 | function getMarketHashName(item) { 991 | if (item == null) { 992 | return null; 993 | } 994 | 995 | if (item.description != null && item.description.market_hash_name != null) { 996 | return item.description.market_hash_name; 997 | } 998 | 999 | if (item.description != null && item.description.name != null) { 1000 | return item.description.name; 1001 | } 1002 | 1003 | if (item.market_hash_name != null) { 1004 | return item.market_hash_name; 1005 | } 1006 | 1007 | if (item.name != null) { 1008 | return item.name; 1009 | } 1010 | 1011 | return null; 1012 | } 1013 | 1014 | function getIsCrate(item) { 1015 | if (item == null) { 1016 | return false; 1017 | } 1018 | // This is available on the inventory page. 1019 | const tags = item.tags != null 1020 | ? item.tags 1021 | : item.description != null && item.description.tags != null 1022 | ? item.description.tags 1023 | : null; 1024 | if (tags != null) { 1025 | let isTaggedAsCrate = false; 1026 | tags.forEach((arrayItem) => { 1027 | if (arrayItem.category == 'Type') { 1028 | if (arrayItem.internal_name == 'Supply Crate') { 1029 | isTaggedAsCrate = true; 1030 | } 1031 | } 1032 | }); 1033 | if (isTaggedAsCrate) { 1034 | return true; 1035 | } 1036 | } 1037 | } 1038 | 1039 | function getIsTradingCard(item) { 1040 | if (item == null) { 1041 | return false; 1042 | } 1043 | 1044 | // This is available on the inventory page. 1045 | const tags = item.tags != null 1046 | ? item.tags 1047 | : item.description != null && item.description.tags != null 1048 | ? item.description.tags 1049 | : null; 1050 | if (tags != null) { 1051 | let isTaggedAsTradingCard = false; 1052 | tags.forEach((arrayItem) => { 1053 | if (arrayItem.category == 'item_class') { 1054 | if (arrayItem.internal_name == 'item_class_2') { // trading card. 1055 | isTaggedAsTradingCard = true; 1056 | } 1057 | } 1058 | }); 1059 | if (isTaggedAsTradingCard) { 1060 | return true; 1061 | } 1062 | } 1063 | 1064 | // This is available on the market page. 1065 | if (item.owner_actions != null) { 1066 | for (let i = 0; i < item.owner_actions.length; i++) { 1067 | if (item.owner_actions[i].link == null) { 1068 | continue; 1069 | } 1070 | 1071 | // Cards include a link to the gamecard page. 1072 | // For example: "http://steamcommunity.com/my/gamecards/503820/". 1073 | if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards')) { 1074 | return true; 1075 | } 1076 | } 1077 | } 1078 | 1079 | // A fallback for the market page (only works with language on English). 1080 | if (item.type != null && item.type.toLowerCase().includes('trading card')) { 1081 | return true; 1082 | } 1083 | 1084 | return false; 1085 | } 1086 | 1087 | function getIsFoilTradingCard(item) { 1088 | if (!getIsTradingCard(item)) { 1089 | return false; 1090 | } 1091 | 1092 | // This is available on the inventory page. 1093 | const tags = item.tags != null 1094 | ? item.tags 1095 | : item.description != null && item.description.tags != null 1096 | ? item.description.tags 1097 | : null; 1098 | if (tags != null) { 1099 | let isTaggedAsFoilTradingCard = false; 1100 | tags.forEach((arrayItem) => { 1101 | if (arrayItem.category == 'cardborder' && arrayItem.internal_name == 'cardborder_1') { // foil border. 1102 | isTaggedAsFoilTradingCard = true; 1103 | } 1104 | }); 1105 | if (isTaggedAsFoilTradingCard) { 1106 | return true; 1107 | } 1108 | } 1109 | 1110 | // This is available on the market page. 1111 | if (item.owner_actions != null) { 1112 | for (let i = 0; i < item.owner_actions.length; i++) { 1113 | if (item.owner_actions[i].link == null) { 1114 | continue; 1115 | } 1116 | 1117 | // Cards include a link to the gamecard page. 1118 | // The border parameter specifies the foil cards. 1119 | // For example: "http://steamcommunity.com/my/gamecards/503820/?border=1". 1120 | if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards') && 1121 | item.owner_actions[i].link.toString().toLowerCase().includes('border')) { 1122 | return true; 1123 | } 1124 | } 1125 | } 1126 | 1127 | // A fallback for the market page (only works with language on English). 1128 | if (item.type != null && item.type.toLowerCase().includes('foil trading card')) { 1129 | return true; 1130 | } 1131 | 1132 | return false; 1133 | } 1134 | 1135 | function CalculateFeeAmount(amount, publisherFee, walletInfo) { 1136 | if (walletInfo == null || !walletInfo['wallet_fee']) { 1137 | return { 1138 | fees: 0 1139 | }; 1140 | } 1141 | 1142 | publisherFee = publisherFee == null ? 0 : publisherFee; 1143 | // Since CalculateFeeAmount has a Math.floor, we could be off a cent or two. Let's check: 1144 | let iterations = 0; // shouldn't be needed, but included to be sure nothing unforseen causes us to get stuck 1145 | let nEstimatedAmountOfWalletFundsReceivedByOtherParty = 1146 | parseInt((amount - parseInt(walletInfo['wallet_fee_base'])) / 1147 | (parseFloat(walletInfo['wallet_fee_percent']) + parseFloat(publisherFee) + 1)); 1148 | let bEverUndershot = false; 1149 | let fees = CalculateAmountToSendForDesiredReceivedAmount( 1150 | nEstimatedAmountOfWalletFundsReceivedByOtherParty, 1151 | publisherFee, 1152 | walletInfo 1153 | ); 1154 | while (fees.amount != amount && iterations < 10) { 1155 | if (fees.amount > amount) { 1156 | if (bEverUndershot) { 1157 | fees = CalculateAmountToSendForDesiredReceivedAmount( 1158 | nEstimatedAmountOfWalletFundsReceivedByOtherParty - 1, 1159 | publisherFee, 1160 | walletInfo 1161 | ); 1162 | fees.steam_fee += amount - fees.amount; 1163 | fees.fees += amount - fees.amount; 1164 | fees.amount = amount; 1165 | break; 1166 | } else { 1167 | nEstimatedAmountOfWalletFundsReceivedByOtherParty--; 1168 | } 1169 | } else { 1170 | bEverUndershot = true; 1171 | nEstimatedAmountOfWalletFundsReceivedByOtherParty++; 1172 | } 1173 | fees = CalculateAmountToSendForDesiredReceivedAmount( 1174 | nEstimatedAmountOfWalletFundsReceivedByOtherParty, 1175 | publisherFee, 1176 | walletInfo 1177 | ); 1178 | iterations++; 1179 | } 1180 | // fees.amount should equal the passed in amount 1181 | return fees; 1182 | } 1183 | 1184 | // Clamps cur between min and max (inclusive). 1185 | function clamp(cur, min, max) { 1186 | if (cur < min) { 1187 | cur = min; 1188 | } 1189 | 1190 | if (cur > max) { 1191 | cur = max; 1192 | } 1193 | 1194 | return cur; 1195 | } 1196 | 1197 | // Strangely named function, it actually works out the fees and buyer price for a seller price 1198 | function CalculateAmountToSendForDesiredReceivedAmount(receivedAmount, publisherFee, walletInfo) { 1199 | if (walletInfo == null || !walletInfo['wallet_fee']) { 1200 | return { 1201 | amount: receivedAmount 1202 | }; 1203 | } 1204 | 1205 | publisherFee = publisherFee == null ? 0 : publisherFee; 1206 | const nSteamFee = parseInt(Math.floor(Math.max( 1207 | receivedAmount * parseFloat(walletInfo['wallet_fee_percent']), 1208 | walletInfo['wallet_fee_minimum'] 1209 | ) + 1210 | parseInt(walletInfo['wallet_fee_base']))); 1211 | const nPublisherFee = parseInt(Math.floor(publisherFee > 0 ? Math.max(receivedAmount * publisherFee, 1) : 0)); 1212 | const nAmountToSend = receivedAmount + nSteamFee + nPublisherFee; 1213 | return { 1214 | steam_fee: nSteamFee, 1215 | publisher_fee: nPublisherFee, 1216 | fees: nSteamFee + nPublisherFee, 1217 | amount: parseInt(nAmountToSend) 1218 | }; 1219 | } 1220 | 1221 | function readCookie(name) { 1222 | const nameEQ = `${name}=`; 1223 | const ca = document.cookie.split(';'); 1224 | for (let i = 0; i < ca.length; i++) { 1225 | let c = ca[i]; 1226 | while (c.charAt(0) == ' ') { 1227 | c = c.substring(1, c.length); 1228 | } 1229 | if (c.indexOf(nameEQ) == 0) { 1230 | return decodeURIComponent(c.substring(nameEQ.length, c.length)); 1231 | } 1232 | } 1233 | return null; 1234 | } 1235 | 1236 | function isRetryMessage(message) { 1237 | const messageList = [ 1238 | 'You cannot sell any items until your previous action completes.', 1239 | 'There was a problem listing your item. Refresh the page and try again.', 1240 | 'We were unable to contact the game\'s item server. The game\'s item server may be down or Steam may be experiencing temporary connectivity issues. Your listing has not been created. Refresh the page and try again.' 1241 | ]; 1242 | 1243 | return messageList.indexOf(message) !== -1; 1244 | } 1245 | //#endregion 1246 | 1247 | //#region Logging 1248 | let userScrolled = false; 1249 | const logger = document.createElement('div'); 1250 | logger.setAttribute('id', 'logger'); 1251 | 1252 | function updateScroll() { 1253 | if (!userScrolled) { 1254 | const element = document.getElementById('logger'); 1255 | element.scrollTop = element.scrollHeight; 1256 | } 1257 | } 1258 | 1259 | function logDOM(text) { 1260 | logger.innerHTML += `${text}
`; 1261 | 1262 | updateScroll(); 1263 | } 1264 | 1265 | function logConsole(text) { 1266 | if (enableConsoleLog) { 1267 | console.log(text); 1268 | } 1269 | } 1270 | //#endregion 1271 | 1272 | //#region Inventory 1273 | if (currentPage == PAGE_INVENTORY) { 1274 | 1275 | function onQueueDrain() { 1276 | if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0 && boosterQueue.length() == 0) { 1277 | $('#inventory_items_spinner').remove(); 1278 | } 1279 | } 1280 | 1281 | function updateTotals() { 1282 | if ($('#loggerTotal').length == 0) { 1283 | $(logger).parent().append('
'); 1284 | } 1285 | 1286 | const totals = document.getElementById('loggerTotal'); 1287 | totals.innerHTML = ''; 1288 | 1289 | if (totalPriceWithFeesOnMarket > 0) { 1290 | totals.innerHTML += `
Total listed for ${formatPrice(totalPriceWithFeesOnMarket)}, you will receive ${formatPrice(totalPriceWithoutFeesOnMarket)}.
`; 1291 | } 1292 | if (totalScrap > 0) { 1293 | totals.innerHTML += `
Total scrap ${totalScrap}.
`; 1294 | } 1295 | } 1296 | 1297 | const sellQueue = async.queue( 1298 | (task, next) => { 1299 | totalNumberOfProcessedQueueItems++; 1300 | 1301 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1302 | const itemId = task.item.assetid || task.item.id; 1303 | const itemName = task.item.name || task.item.description.name; 1304 | const itemNameWithAmount = task.item.amount == 1 ? itemName : `${task.item.amount}x ${itemName}`; 1305 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1306 | 1307 | if (getSettingWithDefault(SETTING_PRICE_MIN_LIST_PRICE) * 100 >= market.getPriceIncludingFees(task.sellPrice)) { 1308 | logDOM(`${padLeft} - ${itemNameWithAmount} is not listed due to ignoring price settings.`); 1309 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_PRICE_NOT_CHECKED); 1310 | next(); 1311 | return; 1312 | } 1313 | 1314 | market.sellItem( 1315 | task.item, 1316 | task.sellPrice, 1317 | (error, data) => { 1318 | const success = Boolean(data?.success); 1319 | const message = data?.message || ''; 1320 | 1321 | const callback = () => setTimeout(() => next(), getRandomInt(1000, 1500)); 1322 | 1323 | if (success) { 1324 | logDOM(`${padLeft} - ${itemNameWithAmount} listed for ${formatPrice(market.getPriceIncludingFees(task.sellPrice) * task.item.amount)}, you will receive ${formatPrice(task.sellPrice * task.item.amount)}.`); 1325 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1326 | 1327 | totalPriceWithoutFeesOnMarket += task.sellPrice * task.item.amount; 1328 | totalPriceWithFeesOnMarket += market.getPriceIncludingFees(task.sellPrice) * task.item.amount; 1329 | 1330 | updateTotals(); 1331 | callback() 1332 | 1333 | return; 1334 | } 1335 | 1336 | if (message && isRetryMessage(message)) { 1337 | logDOM(`${padLeft} - ${itemNameWithAmount} retrying listing because: ${message.charAt(0).toLowerCase()}${message.slice(1)}`); 1338 | 1339 | totalNumberOfProcessedQueueItems--; 1340 | sellQueue.unshift(task); 1341 | sellQueue.pause(); 1342 | 1343 | setTimeout(() => sellQueue.resume(), getRandomInt(30000, 45000)); 1344 | callback(); 1345 | 1346 | return; 1347 | } 1348 | 1349 | logDOM(`${padLeft} - ${itemNameWithAmount} not added to market${message ? ` because: ${message.charAt(0).toLowerCase()}${message.slice(1)}` : '.'}`); 1350 | $(`#${task.item.appid}_${task.item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1351 | 1352 | callback(); 1353 | } 1354 | ); 1355 | }, 1356 | 1 1357 | ); 1358 | 1359 | sellQueue.drain(() => { 1360 | onQueueDrain(); 1361 | }); 1362 | 1363 | function sellAllItems() { 1364 | loadAllInventories().then( 1365 | () => { 1366 | const items = getInventoryItems(); 1367 | const filteredItems = []; 1368 | 1369 | items.forEach((item) => { 1370 | if (!item.marketable) { 1371 | return; 1372 | } 1373 | 1374 | filteredItems.push(item); 1375 | }); 1376 | 1377 | sellItems(filteredItems); 1378 | }, 1379 | () => { 1380 | logDOM('Could not retrieve the inventory...'); 1381 | } 1382 | ); 1383 | } 1384 | 1385 | function sellAllDuplicateItems() { 1386 | loadAllInventories().then( 1387 | () => { 1388 | const items = getInventoryItems(); 1389 | const marketableItems = []; 1390 | let filteredItems = []; 1391 | 1392 | items.forEach((item) => { 1393 | if (!item.marketable) { 1394 | return; 1395 | } 1396 | 1397 | marketableItems.push(item); 1398 | }); 1399 | 1400 | filteredItems = marketableItems.filter((e, i) => marketableItems.map((m) => m.classid).indexOf(e.classid) !== i); 1401 | 1402 | sellItems(filteredItems); 1403 | }, 1404 | () => { 1405 | logDOM('Could not retrieve the inventory...'); 1406 | } 1407 | ); 1408 | } 1409 | 1410 | function gemAllDuplicateItems() { 1411 | loadAllInventories().then( 1412 | () => { 1413 | const items = getInventoryItems(); 1414 | let filteredItems = []; 1415 | let numberOfQueuedItems = 0; 1416 | 1417 | filteredItems = items.filter((e, i) => items.map((m) => m.classid).indexOf(e.classid) !== i); 1418 | 1419 | filteredItems.forEach((item) => { 1420 | if (item.queued != null) { 1421 | return; 1422 | } 1423 | 1424 | if (item.owner_actions == null) { 1425 | return; 1426 | } 1427 | 1428 | let canTurnIntoGems = false; 1429 | for (const owner_action in item.owner_actions) { 1430 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 1431 | canTurnIntoGems = true; 1432 | } 1433 | } 1434 | 1435 | if (!canTurnIntoGems) { 1436 | return; 1437 | } 1438 | 1439 | item.queued = true; 1440 | scrapQueue.push(item); 1441 | numberOfQueuedItems++; 1442 | }); 1443 | 1444 | if (numberOfQueuedItems > 0) { 1445 | totalNumberOfQueuedItems += numberOfQueuedItems; 1446 | 1447 | $('#inventory_items_spinner').remove(); 1448 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1449 | }
Processing ${numberOfQueuedItems} items
` + 1450 | '
'); 1451 | } 1452 | }, 1453 | () => { 1454 | logDOM('Could not retrieve the inventory...'); 1455 | } 1456 | ); 1457 | } 1458 | 1459 | function sellAllCards() { 1460 | loadAllInventories().then( 1461 | () => { 1462 | const items = getInventoryItems(); 1463 | const filteredItems = []; 1464 | 1465 | items.forEach((item) => { 1466 | if (!getIsTradingCard(item) || !item.marketable) { 1467 | return; 1468 | } 1469 | 1470 | filteredItems.push(item); 1471 | }); 1472 | 1473 | sellItems(filteredItems); 1474 | }, 1475 | () => { 1476 | logDOM('Could not retrieve the inventory...'); 1477 | } 1478 | ); 1479 | } 1480 | 1481 | function sellAllCrates() { 1482 | loadAllInventories().then( 1483 | () => { 1484 | const items = getInventoryItems(); 1485 | const filteredItems = []; 1486 | items.forEach((item) => { 1487 | if (!getIsCrate(item) || !item.marketable) { 1488 | return; 1489 | } 1490 | filteredItems.push(item); 1491 | }); 1492 | 1493 | sellItems(filteredItems); 1494 | }, 1495 | () => { 1496 | logDOM('Could not retrieve the inventory...'); 1497 | } 1498 | ); 1499 | } 1500 | 1501 | const scrapQueue = async.queue((item, next) => { 1502 | scrapQueueWorker(item, (success) => { 1503 | if (success) { 1504 | setTimeout(() => { 1505 | next(); 1506 | }, 250); 1507 | } else { 1508 | const delay = numberOfFailedRequests > 1 1509 | ? getRandomInt(30000, 45000) 1510 | : getRandomInt(1000, 1500); 1511 | 1512 | if (numberOfFailedRequests > 3) { 1513 | numberOfFailedRequests = 0; 1514 | } 1515 | 1516 | setTimeout(() => { 1517 | next(); 1518 | }, delay); 1519 | } 1520 | }); 1521 | }, 1); 1522 | 1523 | scrapQueue.drain(() => { 1524 | onQueueDrain(); 1525 | }); 1526 | 1527 | function scrapQueueWorker(item, callback) { 1528 | const itemName = item.name || item.description.name; 1529 | const itemId = item.assetid || item.id; 1530 | 1531 | market.getGooValue( 1532 | item, 1533 | (err, goo) => { 1534 | totalNumberOfProcessedQueueItems++; 1535 | 1536 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1537 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1538 | 1539 | if (err != ERROR_SUCCESS) { 1540 | logConsole(`Failed to get gems value for ${itemName}`); 1541 | logDOM(`${padLeft} - ${itemName} not turned into gems due to missing gems value.`); 1542 | 1543 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1544 | return callback(false); 1545 | } 1546 | 1547 | item.goo_value_expected = parseInt(goo.goo_value, 10); 1548 | 1549 | market.grindIntoGoo( 1550 | item, 1551 | (err) => { 1552 | if (err != ERROR_SUCCESS) { 1553 | logConsole(`Failed to turn item into gems for ${itemName}`); 1554 | logDOM(`${padLeft} - ${itemName} not turned into gems due to unknown error.`); 1555 | 1556 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1557 | return callback(false); 1558 | } 1559 | 1560 | logConsole('============================'); 1561 | logConsole(itemName); 1562 | logConsole(`Turned into ${goo.goo_value} gems`); 1563 | logDOM(`${padLeft} - ${itemName} turned into ${item.goo_value_expected} gems.`); 1564 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1565 | 1566 | totalScrap += item.goo_value_expected; 1567 | updateTotals(); 1568 | 1569 | callback(true); 1570 | } 1571 | ); 1572 | } 1573 | ); 1574 | } 1575 | 1576 | const boosterQueue = async.queue((item, next) => { 1577 | boosterQueueWorker(item, (success) => { 1578 | if (success) { 1579 | setTimeout(() => { 1580 | next(); 1581 | }, 250); 1582 | } else { 1583 | const delay = numberOfFailedRequests > 1 1584 | ? getRandomInt(30000, 45000) 1585 | : getRandomInt(1000, 1500); 1586 | 1587 | if (numberOfFailedRequests > 3) { 1588 | numberOfFailedRequests = 0; 1589 | } 1590 | 1591 | setTimeout(() => { 1592 | next(); 1593 | }, delay); 1594 | } 1595 | }); 1596 | }, 1); 1597 | 1598 | boosterQueue.drain(() => { 1599 | onQueueDrain(); 1600 | }); 1601 | 1602 | function boosterQueueWorker(item, callback) { 1603 | const itemName = item.name || item.description.name; 1604 | const itemId = item.assetid || item.id; 1605 | 1606 | market.unpackBoosterPack( 1607 | item, 1608 | (err) => { 1609 | totalNumberOfProcessedQueueItems++; 1610 | 1611 | const digits = getNumberOfDigits(totalNumberOfQueuedItems); 1612 | const padLeft = `${padLeftZero(`${totalNumberOfProcessedQueueItems}`, digits)} / ${totalNumberOfQueuedItems}`; 1613 | 1614 | if (err != ERROR_SUCCESS) { 1615 | logConsole(`Failed to unpack booster pack ${itemName}`); 1616 | logDOM(`${padLeft} - ${itemName} not unpacked.`); 1617 | 1618 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_ERROR); 1619 | return callback(false); 1620 | } 1621 | 1622 | logDOM(`${padLeft} - ${itemName} unpacked.`); 1623 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_SUCCESS); 1624 | 1625 | callback(true); 1626 | } 1627 | ); 1628 | } 1629 | 1630 | 1631 | // Turns the selected items into gems. 1632 | function turnSelectedItemsIntoGems() { 1633 | const ids = getSelectedItems(); 1634 | 1635 | loadAllInventories().then(() => { 1636 | const items = getInventoryItems(); 1637 | 1638 | let numberOfQueuedItems = 0; 1639 | items.forEach((item) => { 1640 | // Ignored queued items. 1641 | if (item.queued != null) { 1642 | return; 1643 | } 1644 | 1645 | if (item.owner_actions == null) { 1646 | return; 1647 | } 1648 | 1649 | let canTurnIntoGems = false; 1650 | for (const owner_action in item.owner_actions) { 1651 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 1652 | canTurnIntoGems = true; 1653 | } 1654 | } 1655 | 1656 | if (!canTurnIntoGems) { 1657 | return; 1658 | } 1659 | 1660 | const itemId = item.assetid || item.id; 1661 | if (ids.indexOf(itemId) !== -1) { 1662 | item.queued = true; 1663 | scrapQueue.push(item); 1664 | numberOfQueuedItems++; 1665 | } 1666 | }); 1667 | 1668 | if (numberOfQueuedItems > 0) { 1669 | totalNumberOfQueuedItems += numberOfQueuedItems; 1670 | 1671 | $('#inventory_items_spinner').remove(); 1672 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1673 | }
Processing ${numberOfQueuedItems} items
` + 1674 | '
'); 1675 | } 1676 | }, () => { 1677 | logDOM('Could not retrieve the inventory...'); 1678 | }); 1679 | } 1680 | 1681 | // Unpacks the selected booster packs. 1682 | function unpackSelectedBoosterPacks() { 1683 | const ids = getSelectedItems(); 1684 | 1685 | loadAllInventories().then(() => { 1686 | const items = getInventoryItems(); 1687 | 1688 | let numberOfQueuedItems = 0; 1689 | items.forEach((item) => { 1690 | // Ignored queued items. 1691 | if (item.queued != null) { 1692 | return; 1693 | } 1694 | 1695 | if (item.owner_actions == null) { 1696 | return; 1697 | } 1698 | 1699 | let canOpenBooster = false; 1700 | for (const owner_action in item.owner_actions) { 1701 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { 1702 | canOpenBooster = true; 1703 | } 1704 | } 1705 | 1706 | if (!canOpenBooster) { 1707 | return; 1708 | } 1709 | 1710 | const itemId = item.assetid || item.id; 1711 | if (ids.indexOf(itemId) !== -1) { 1712 | item.queued = true; 1713 | boosterQueue.push(item); 1714 | numberOfQueuedItems++; 1715 | } 1716 | }); 1717 | 1718 | if (numberOfQueuedItems > 0) { 1719 | totalNumberOfQueuedItems += numberOfQueuedItems; 1720 | 1721 | $('#inventory_items_spinner').remove(); 1722 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1723 | }
Processing ${numberOfQueuedItems} items
` + 1724 | '
'); 1725 | } 1726 | }, () => { 1727 | logDOM('Could not retrieve the inventory...'); 1728 | }); 1729 | } 1730 | 1731 | function sellSelectedItems() { 1732 | getInventorySelectedMarketableItems((items) => { 1733 | sellItems(items); 1734 | }); 1735 | } 1736 | 1737 | function canSellSelectedItemsManually(items) { 1738 | // We have to construct an URL like this 1739 | // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 1740 | const contextid = items[0].contextid; 1741 | let hasInvalidItem = false; 1742 | 1743 | items.forEach((item) => { 1744 | if (item.contextid != contextid || item.commodity == false) { 1745 | hasInvalidItem = true; 1746 | } 1747 | }); 1748 | 1749 | return !hasInvalidItem; 1750 | } 1751 | 1752 | function sellSelectedItemsManually() { 1753 | getInventorySelectedMarketableItems((items) => { 1754 | // We have to construct an URL like this 1755 | // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 1756 | 1757 | const appid = items[0].appid; 1758 | const contextid = items[0].contextid; 1759 | 1760 | const itemsWithQty = {}; 1761 | 1762 | items.forEach((item) => { 1763 | itemsWithQty[item.market_hash_name] = itemsWithQty[item.market_hash_name] + 1 || 1; 1764 | }); 1765 | 1766 | let itemsString = ''; 1767 | for (const itemName in itemsWithQty) { 1768 | itemsString += `&items[]=${encodeURIComponent(itemName)}&qty[]=${itemsWithQty[itemName]}`; 1769 | } 1770 | 1771 | const baseUrl = `${window.location.origin}/market/multisell`; 1772 | const redirectUrl = `${baseUrl}?appid=${appid}&contextid=${contextid}${itemsString}`; 1773 | 1774 | const dialog = unsafeWindow.ShowDialog('Steam Economy Enhancer', ``); 1775 | dialog.OnDismiss(() => { 1776 | items.forEach((item) => { 1777 | const itemId = item.assetid || item.id; 1778 | $(`#${item.appid}_${item.contextid}_${itemId}`).css('background', COLOR_PENDING); 1779 | }); 1780 | }); 1781 | }); 1782 | } 1783 | 1784 | function sellItems(items) { 1785 | if (items.length == 0) { 1786 | logDOM('These items cannot be added to the market...'); 1787 | 1788 | return; 1789 | } 1790 | 1791 | let numberOfQueuedItems = 0; 1792 | 1793 | items.forEach((item) => { 1794 | // Ignored queued items. 1795 | if (item.queued != null) { 1796 | return; 1797 | } 1798 | 1799 | item.queued = true; 1800 | item.ignoreErrors = false; 1801 | itemQueue.push(item); 1802 | numberOfQueuedItems++; 1803 | }); 1804 | 1805 | if (numberOfQueuedItems > 0) { 1806 | totalNumberOfQueuedItems += numberOfQueuedItems; 1807 | 1808 | $('#inventory_items_spinner').remove(); 1809 | $('#inventory_sell_buttons').append(`
${spinnerBlock 1810 | }
Processing ${numberOfQueuedItems} items
` + 1811 | '
'); 1812 | } 1813 | } 1814 | 1815 | const itemQueue = async.queue((item, next) => { 1816 | itemQueueWorker( 1817 | item, 1818 | item.ignoreErrors, 1819 | (success, cached) => { 1820 | if (success) { 1821 | setTimeout(() => next(), cached ? 0 : getRandomInt(1000, 1500)); 1822 | } else { 1823 | if (!item.ignoreErrors) { 1824 | item.ignoreErrors = true; 1825 | itemQueue.push(item); 1826 | } 1827 | 1828 | const delay = numberOfFailedRequests > 1 ? getRandomInt(30000, 45000) : getRandomInt(1000, 1500); 1829 | numberOfFailedRequests = numberOfFailedRequests > 3 ? 0 : numberOfFailedRequests; 1830 | 1831 | setTimeout(() => next(), cached ? 0 : delay); 1832 | } 1833 | } 1834 | ); 1835 | }, 1); 1836 | 1837 | function itemQueueWorker(item, ignoreErrors, callback) { 1838 | const priceInfo = getPriceInformationFromItem(item); 1839 | 1840 | let failed = 0; 1841 | const itemName = item.name || item.description.name; 1842 | 1843 | market.getPriceHistory( 1844 | item, 1845 | true, 1846 | (err, history, cachedHistory) => { 1847 | if (err) { 1848 | logConsole(`Failed to get price history for ${itemName}`); 1849 | 1850 | if (err != ERROR_SUCCESS) { 1851 | failed += 1; 1852 | } 1853 | } 1854 | 1855 | market.getItemOrdersHistogram( 1856 | item, 1857 | true, 1858 | (err, histogram, cachedListings) => { 1859 | if (err) { 1860 | logConsole(`Failed to get orders histogram for ${itemName}`); 1861 | 1862 | if (err != ERROR_SUCCESS) { 1863 | failed += 1; 1864 | } 1865 | } 1866 | 1867 | if (failed > 0 && !ignoreErrors) { 1868 | return callback(false, cachedHistory && cachedListings); 1869 | } 1870 | 1871 | logConsole('============================'); 1872 | logConsole(itemName); 1873 | 1874 | const sellPrice = calculateSellPriceBeforeFees( 1875 | history, 1876 | histogram, 1877 | true, 1878 | priceInfo.minPriceBeforeFees, 1879 | priceInfo.maxPriceBeforeFees 1880 | ); 1881 | 1882 | 1883 | logConsole(`Sell price: ${sellPrice / 100.0} (${market.getPriceIncludingFees(sellPrice) / 100.0})`); 1884 | 1885 | sellQueue.push({ 1886 | item: item, 1887 | sellPrice: sellPrice 1888 | }); 1889 | 1890 | return callback(true, cachedHistory && cachedListings); 1891 | } 1892 | ); 1893 | } 1894 | ); 1895 | } 1896 | 1897 | // Initialize the inventory UI. 1898 | function initializeInventoryUI() { 1899 | const isOwnInventory = unsafeWindow.g_ActiveUser.strSteamId == unsafeWindow.g_steamID; 1900 | let previousSelection = -1; // To store the index of the previous selection. 1901 | updateInventoryUI(isOwnInventory); 1902 | 1903 | $('.games_list_tabs').on( 1904 | 'click', 1905 | '*', 1906 | () => { 1907 | updateInventoryUI(isOwnInventory); 1908 | } 1909 | ); 1910 | 1911 | // Ignore selection on other user's inventories. 1912 | if (!isOwnInventory) { 1913 | return; 1914 | } 1915 | 1916 | // Steam adds 'display:none' to items while searching. These should not be selected while using shift/ctrl. 1917 | const filter = '.itemHolder:not([style*=none])'; 1918 | $('#inventories').selectable({ 1919 | filter: filter, 1920 | selecting: function (e, ui) { 1921 | // Get selected item index. 1922 | const selectedIndex = $(ui.selecting.tagName, e.target).index(ui.selecting); 1923 | 1924 | // If shift key was pressed and there is previous - select them all. 1925 | if (e.shiftKey && previousSelection > -1) { 1926 | $(ui.selecting.tagName, e.target). 1927 | slice( 1928 | Math.min(previousSelection, selectedIndex), 1929 | 1 + Math.max(previousSelection, selectedIndex) 1930 | ).each(function () { 1931 | if ($(this).is(filter)) { 1932 | $(this).addClass('ui-selected'); 1933 | } 1934 | }); 1935 | previousSelection = -1; // Reset previous. 1936 | } else { 1937 | previousSelection = selectedIndex; // Save previous. 1938 | } 1939 | }, 1940 | selected: function () { 1941 | updateButtons(); 1942 | } 1943 | }); 1944 | 1945 | if (typeof unsafeWindow.CInventory !== 'undefined') { 1946 | const originalSelectItem = unsafeWindow.CInventory.prototype.SelectItem; 1947 | 1948 | unsafeWindow.CInventory.prototype.SelectItem = function (event, elItem, rgItem) { 1949 | originalSelectItem.apply(this, arguments); 1950 | 1951 | updateButtons(); 1952 | updateInventorySelection(rgItem); 1953 | }; 1954 | } 1955 | } 1956 | 1957 | // Gets the selected items in the inventory. 1958 | function getSelectedItems() { 1959 | const ids = []; 1960 | $('.inventory_ctn').each(function () { 1961 | $(this).find('.inventory_page').each(function () { 1962 | const inventory_page = this; 1963 | 1964 | $(inventory_page).find('.itemHolder.ui-selected:not([style*=none])').each(function () { 1965 | $(this).find('.item').each(function () { 1966 | const matches = this.id.match(/_(-?\d+)$/); 1967 | if (matches) { 1968 | ids.push(matches[1]); 1969 | } 1970 | }); 1971 | }); 1972 | }); 1973 | }); 1974 | 1975 | return ids; 1976 | } 1977 | 1978 | // Gets the selected and marketable items in the inventory. 1979 | function getInventorySelectedMarketableItems(callback) { 1980 | const ids = getSelectedItems(); 1981 | 1982 | loadAllInventories().then(() => { 1983 | const items = getInventoryItems(); 1984 | const filteredItems = []; 1985 | 1986 | items.forEach((item) => { 1987 | if (!item.marketable) { 1988 | return; 1989 | } 1990 | 1991 | const itemId = item.assetid || item.id; 1992 | if (ids.indexOf(itemId) !== -1) { 1993 | filteredItems.push(item); 1994 | } 1995 | }); 1996 | 1997 | callback(filteredItems); 1998 | }, () => { 1999 | logDOM('Could not retrieve the inventory...'); 2000 | }); 2001 | } 2002 | 2003 | // Gets the selected and gemmable items in the inventory. 2004 | function getInventorySelectedGemsItems(callback) { 2005 | const ids = getSelectedItems(); 2006 | 2007 | loadAllInventories().then(() => { 2008 | const items = getInventoryItems(); 2009 | const filteredItems = []; 2010 | 2011 | items.forEach((item) => { 2012 | let canTurnIntoGems = false; 2013 | for (const owner_action in item.owner_actions) { 2014 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { 2015 | canTurnIntoGems = true; 2016 | } 2017 | } 2018 | 2019 | if (!canTurnIntoGems) { 2020 | return; 2021 | } 2022 | 2023 | const itemId = item.assetid || item.id; 2024 | if (ids.indexOf(itemId) !== -1) { 2025 | filteredItems.push(item); 2026 | } 2027 | }); 2028 | 2029 | callback(filteredItems); 2030 | }, () => { 2031 | logDOM('Could not retrieve the inventory...'); 2032 | }); 2033 | } 2034 | 2035 | // Gets the selected and booster pack items in the inventory. 2036 | function getInventorySelectedBoosterPackItems(callback) { 2037 | const ids = getSelectedItems(); 2038 | 2039 | loadAllInventories().then(() => { 2040 | const items = getInventoryItems(); 2041 | const filteredItems = []; 2042 | 2043 | items.forEach((item) => { 2044 | let canOpenBooster = false; 2045 | for (const owner_action in item.owner_actions) { 2046 | if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { 2047 | canOpenBooster = true; 2048 | } 2049 | } 2050 | 2051 | if (!canOpenBooster) { 2052 | return; 2053 | } 2054 | 2055 | const itemId = item.assetid || item.id; 2056 | if (ids.indexOf(itemId) !== -1) { 2057 | filteredItems.push(item); 2058 | } 2059 | }); 2060 | 2061 | callback(filteredItems); 2062 | }, () => { 2063 | logDOM('Could not retrieve the inventory...'); 2064 | }); 2065 | } 2066 | 2067 | // Updates the (selected) sell ... items button. 2068 | function updateSellSelectedButton() { 2069 | getInventorySelectedMarketableItems((items) => { 2070 | const selectedItems = items.length; 2071 | if (items.length == 0) { 2072 | $('.sell_selected').hide(); 2073 | $('.sell_manual').hide(); 2074 | } else { 2075 | $('.sell_selected').show(); 2076 | if (canSellSelectedItemsManually(items)) { 2077 | $('.sell_manual').show(); 2078 | $('.sell_manual > span').text(`Sell ${selectedItems}${selectedItems == 1 ? ' Item Manual' : ' Items Manual'}`); 2079 | } else { 2080 | $('.sell_manual').hide(); 2081 | } 2082 | $('.sell_selected > span').text(`Sell ${selectedItems}${selectedItems == 1 ? ' Item' : ' Items'}`); 2083 | } 2084 | }); 2085 | } 2086 | 2087 | // Updates the (selected) turn into ... gems button. 2088 | function updateTurnIntoGemsButton() { 2089 | getInventorySelectedGemsItems((items) => { 2090 | const selectedItems = items.length; 2091 | if (items.length == 0) { 2092 | $('.turn_into_gems').hide(); 2093 | } else { 2094 | $('.turn_into_gems').show(); 2095 | $('.turn_into_gems > span'). 2096 | text(`Turn ${selectedItems}${selectedItems == 1 ? ' Item Into Gems' : ' Items Into Gems'}`); 2097 | } 2098 | }); 2099 | } 2100 | 2101 | // Updates the (selected) open ... booster packs button. 2102 | function updateOpenBoosterPacksButton() { 2103 | getInventorySelectedBoosterPackItems((items) => { 2104 | const selectedItems = items.length; 2105 | if (items.length == 0) { 2106 | $('.unpack_booster_packs').hide(); 2107 | } else { 2108 | $('.unpack_booster_packs').show(); 2109 | $('.unpack_booster_packs > span'). 2110 | text(`Unpack ${selectedItems}${selectedItems == 1 ? ' Booster Pack' : ' Booster Packs'}`); 2111 | } 2112 | }); 2113 | } 2114 | 2115 | function updateButtons() { 2116 | updateSellSelectedButton(); 2117 | updateTurnIntoGemsButton(); 2118 | updateOpenBoosterPacksButton(); 2119 | } 2120 | 2121 | function updateInventorySelection(selectedItem) { 2122 | const item_info = $(`#iteminfo${unsafeWindow.iActiveSelectView}`); 2123 | 2124 | if (!item_info.length) { 2125 | return; 2126 | } 2127 | 2128 | if (item_info.html().indexOf('checkout/sendgift/') > -1) { // Gifts have no market information. 2129 | return; 2130 | } 2131 | 2132 | // Use a 'hard' item id instead of relying on the selected item_info (sometimes Steam temporarily changes the correct item (?)). 2133 | const item_info_id = item_info.attr('id'); 2134 | 2135 | // Move scrap to bottom, this is of little interest. 2136 | const scrap = $(`#${item_info_id}_scrap_content`); 2137 | scrap.next().insertBefore(scrap); 2138 | 2139 | // Skip unmarketable items 2140 | if (!selectedItem.marketable) { 2141 | return; 2142 | } 2143 | 2144 | // Starting at prices are already retrieved in the table. 2145 | //$('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(2)') 2146 | // .remove(); // Starting at: x,xx. 2147 | 2148 | const market_hash_name = getMarketHashName(selectedItem); 2149 | if (market_hash_name == null) { 2150 | return; 2151 | } 2152 | 2153 | const appid = selectedItem.appid; 2154 | const item = { 2155 | appid: parseInt(appid), 2156 | description: { 2157 | market_hash_name: market_hash_name 2158 | } 2159 | }; 2160 | 2161 | const ownerActions = $(`#${item_info_id}_item_owner_actions`); 2162 | 2163 | // Move market link to a button 2164 | ownerActions.append(`View in Community Market`); 2165 | $(`#${item_info_id}_item_market_actions > div:nth-child(1) > div:nth-child(1)`).hide(); 2166 | 2167 | // ownerActions is hidden on other games' inventories, we need to show it to have a "Market" button visible 2168 | ownerActions.show(); 2169 | 2170 | const isBoosterPack = selectedItem.name.toLowerCase().endsWith('booster pack'); 2171 | if (isBoosterPack) { 2172 | 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`; 2173 | ownerActions.append(`
View trading cards in Community Market`); 2174 | } 2175 | 2176 | if (getSettingWithDefault(SETTING_QUICK_SELL_BUTTONS) != 1) { 2177 | return; 2178 | } 2179 | 2180 | // Ignored queued items. 2181 | if (selectedItem.queued != null) { 2182 | return; 2183 | } 2184 | 2185 | market.getItemOrdersHistogram( 2186 | item, 2187 | false, 2188 | (err, histogram) => { 2189 | if (err) { 2190 | logConsole(`Failed to get orders histogram for ${selectedItem.name || selectedItem.description.name}`); 2191 | return; 2192 | } 2193 | 2194 | // Ignored queued items. 2195 | if (selectedItem.queued != null) { 2196 | return; 2197 | } 2198 | 2199 | const groupMain = $(`
2200 |
2201 |
Sell
2202 | ${histogram.sell_order_table} 2203 |
2204 |
2205 |
Buy
2206 | ${histogram.buy_order_table} 2207 |
2208 |
`); 2209 | 2210 | $(`#${item_info_id}_item_market_actions > div`).after(groupMain); 2211 | 2212 | // Generate quick sell buttons. 2213 | let prices = []; 2214 | 2215 | if (histogram != null && histogram.highest_buy_order != null) { 2216 | prices.push(parseInt(histogram.highest_buy_order)); 2217 | } 2218 | 2219 | if (histogram != null && histogram.lowest_sell_order != null) { 2220 | // Transaction volume must be separable into three or more parts (no matter if equal): valve+publisher+seller. 2221 | if (parseInt(histogram.lowest_sell_order) > 3) { 2222 | prices.push(parseInt(histogram.lowest_sell_order) - 1); 2223 | } 2224 | prices.push(parseInt(histogram.lowest_sell_order)); 2225 | } 2226 | 2227 | prices = prices.filter((v, i) => prices.indexOf(v) === i).sort((a, b) => a - b); 2228 | 2229 | let buttons = ' '; 2230 | prices.forEach((e) => { 2231 | buttons += ` 2232 | 2233 | ${formatPrice(e)} 2234 | 2235 | 2236 | `; 2237 | }); 2238 | 2239 | $(`#${item_info_id}_item_market_actions`, item_info).append(buttons); 2240 | 2241 | $(`#${item_info_id}_item_market_actions`, item_info).append(``); 2250 | 2251 | $('.quick_sell').on( 2252 | 'click', 2253 | function () { 2254 | let price = $(this).attr('id').replace('quick_sell', ''); 2255 | price = market.getPriceBeforeFees(price); 2256 | 2257 | totalNumberOfQueuedItems++; 2258 | 2259 | sellQueue.push({ 2260 | item: selectedItem, 2261 | sellPrice: price 2262 | }); 2263 | } 2264 | ); 2265 | 2266 | $('.quick_sell_custom').on( 2267 | 'click', 2268 | () => { 2269 | let price = $('#quick_sell_input', $(`#${item_info_id}_item_market_actions`, item_info)).val() * 100; 2270 | price = market.getPriceBeforeFees(price); 2271 | 2272 | totalNumberOfQueuedItems++; 2273 | 2274 | sellQueue.push({ 2275 | item: selectedItem, 2276 | sellPrice: price 2277 | }); 2278 | } 2279 | ); 2280 | } 2281 | ); 2282 | } 2283 | 2284 | // Update the inventory UI. 2285 | function updateInventoryUI(isOwnInventory) { 2286 | // Remove previous containers (e.g., when a user changes inventory). 2287 | $('#inventory_sell_buttons').remove(); 2288 | $('#see_settings_modal').remove(); 2289 | $('#inventory_reload_button').remove(); 2290 | 2291 | $('#see_settings').remove(); 2292 | $('#global_action_menu'). 2293 | prepend('⬖ Steam Economy Enhancer'); 2294 | $('#see_settings').on('click', '*', () => openSettings()); 2295 | 2296 | const appId = getActiveInventory().m_appid; 2297 | const showMiscOptions = appId == 753; 2298 | const TF2 = appId == 440; 2299 | 2300 | let buttonsHtml = ` 2301 | Sell All Items 2302 | Sell All Duplicate Items 2303 | 2304 | 2305 | `; 2306 | 2307 | if (showMiscOptions) { 2308 | buttonsHtml += ` 2309 | Sell All Cards 2310 | 2315 | `; 2316 | } else if (TF2) { 2317 | buttonsHtml += 'Sell All Crates'; 2318 | } 2319 | 2320 | const sellButtons = $(`
${buttonsHtml}
`); 2321 | 2322 | const reloadButton = 2323 | $('Reload Inventory'); 2324 | 2325 | const logo = $('#inventory_logos')[0]; 2326 | logo.style.height = 'auto'; 2327 | logo.style.maxHeight = 'unset'; 2328 | 2329 | $('#inventory_applogo').hide(); // Hide the Steam/game logo, we don't need to see it twice. 2330 | $('#inventory_applogo').after(logger); 2331 | 2332 | 2333 | $('#logger').on( 2334 | 'scroll', 2335 | () => { 2336 | const hasUserScrolledToBottom = 2337 | $('#logger').prop('scrollHeight') - $('#logger').prop('clientHeight') <= 2338 | $('#logger').prop('scrollTop') + 1; 2339 | userScrolled = !hasUserScrolledToBottom; 2340 | } 2341 | ); 2342 | 2343 | // Only add buttons on the user's inventory. 2344 | if (isOwnInventory) { 2345 | $('#inventory_applogo').after(sellButtons); 2346 | 2347 | // Add bindings to sell buttons. 2348 | $('.sell_all').on( 2349 | 'click', 2350 | '*', 2351 | () => { 2352 | sellAllItems(); 2353 | } 2354 | ); 2355 | $('.sell_selected').on('click', '*', sellSelectedItems); 2356 | $('.sell_all_duplicates').on('click', '*', sellAllDuplicateItems); 2357 | $('.gem_all_duplicates').on('click', '*', gemAllDuplicateItems); 2358 | $('.sell_manual').on('click', '*', sellSelectedItemsManually); 2359 | $('.sell_all_cards').on('click', '*', sellAllCards); 2360 | $('.sell_all_crates').on('click', '*', sellAllCrates); 2361 | $('.turn_into_gems').on('click', '*', turnSelectedItemsIntoGems); 2362 | $('.unpack_booster_packs').on('click', '*', unpackSelectedBoosterPacks); 2363 | 2364 | } 2365 | 2366 | $('.inventory_rightnav').prepend(reloadButton); 2367 | $('.reload_inventory').on( 2368 | 'click', 2369 | '*', 2370 | () => { 2371 | window.location.reload(); 2372 | } 2373 | ); 2374 | 2375 | loadAllInventories().then( 2376 | () => { 2377 | const updateInventoryPrices = function () { 2378 | if (getSettingWithDefault(SETTING_INVENTORY_PRICE_LABELS) == 1) { 2379 | setInventoryPrices(getInventoryItems()); 2380 | } 2381 | }; 2382 | 2383 | // Load after the inventory is loaded. 2384 | updateInventoryPrices(); 2385 | 2386 | $('#inventory_pagecontrols').observe( 2387 | 'childlist', 2388 | '*', 2389 | () => { 2390 | updateInventoryPrices(); 2391 | } 2392 | ); 2393 | }, 2394 | () => { 2395 | logDOM('Could not retrieve the inventory...'); 2396 | } 2397 | ); 2398 | } 2399 | 2400 | // Loads the specified inventories. 2401 | function loadInventories(inventories) { 2402 | return new Promise((resolve) => { 2403 | inventories.reduce( 2404 | (promise, inventory) => { 2405 | return promise.then(() => { 2406 | // return inventory.LoadCompleteInventory().done(() => { }); 2407 | 2408 | // Workaround, until Steam fixes the issue with LoadCompleteInventory. 2409 | 2410 | if (inventory.m_bFullyLoaded) { 2411 | return Promise.resolve(); 2412 | } 2413 | 2414 | if (!inventory.m_promiseLoadCompleteInventory) { 2415 | inventory.m_promiseLoadCompleteInventory = inventory.LoadUntilConditionMet(() => inventory.m_bFullyLoaded, 2000); 2416 | } 2417 | 2418 | return inventory.m_promiseLoadCompleteInventory.done(() => { }); 2419 | }); 2420 | }, 2421 | Promise.resolve() 2422 | ); 2423 | 2424 | resolve(); 2425 | }); 2426 | } 2427 | 2428 | // Loads all inventories. 2429 | function loadAllInventories() { 2430 | const items = []; 2431 | 2432 | for (const child in getActiveInventory().m_rgChildInventories) { 2433 | items.push(getActiveInventory().m_rgChildInventories[child]); 2434 | } 2435 | items.push(getActiveInventory()); 2436 | 2437 | return loadInventories(items); 2438 | } 2439 | 2440 | // Gets the inventory items from the active inventory. 2441 | function getInventoryItems() { 2442 | const arr = []; 2443 | 2444 | for (const child in getActiveInventory().m_rgChildInventories) { 2445 | for (const key in getActiveInventory().m_rgChildInventories[child].m_rgAssets) { 2446 | const value = getActiveInventory().m_rgChildInventories[child].m_rgAssets[key]; 2447 | if (typeof value === 'object') { 2448 | // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. 2449 | Object.assign(value, value.description); 2450 | // Includes the id of the inventory item. 2451 | value['id'] = key; 2452 | value['assetid'] = key; 2453 | arr.push(value); 2454 | } 2455 | } 2456 | } 2457 | 2458 | // Some inventories (e.g. BattleBlock Theater) do not have child inventories, they have just one. 2459 | for (const key in getActiveInventory().m_rgAssets) { 2460 | const value = getActiveInventory().m_rgAssets[key]; 2461 | if (typeof value === 'object') { 2462 | // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. 2463 | Object.assign(value, value.description); 2464 | // Includes the id of the inventory item. 2465 | value['id'] = key; 2466 | value['assetid'] = key; 2467 | arr.push(value); 2468 | } 2469 | } 2470 | 2471 | return arr; 2472 | } 2473 | } 2474 | //#endregion 2475 | 2476 | //#region Inventory + Tradeoffer 2477 | if (currentPage == PAGE_INVENTORY || currentPage == PAGE_TRADEOFFER) { 2478 | 2479 | // Gets the active inventory. 2480 | function getActiveInventory() { 2481 | return unsafeWindow.g_ActiveInventory; 2482 | } 2483 | 2484 | // Sets the prices for the items. 2485 | function setInventoryPrices(items) { 2486 | inventoryPriceQueue.kill(); 2487 | 2488 | items.forEach((item) => { 2489 | if (!item.marketable) { 2490 | return; 2491 | } 2492 | 2493 | if (!$(item.element).is(':visible')) { 2494 | return; 2495 | } 2496 | 2497 | inventoryPriceQueue.push(item); 2498 | }); 2499 | } 2500 | 2501 | const inventoryPriceQueue = async.queue( 2502 | (item, next) => { 2503 | inventoryPriceQueueWorker( 2504 | item, 2505 | false, 2506 | (success, cached) => { 2507 | if (success) { 2508 | setTimeout(() => next(), cached ? 0 : getRandomInt(1000, 1500)); 2509 | } else { 2510 | if (!item.ignoreErrors) { 2511 | item.ignoreErrors = true; 2512 | inventoryPriceQueue.push(item); 2513 | } 2514 | 2515 | numberOfFailedRequests++; 2516 | 2517 | const delay = numberOfFailedRequests > 1 ? getRandomInt(30000, 45000) : getRandomInt(1000, 1500); 2518 | numberOfFailedRequests = numberOfFailedRequests > 3 ? 0 : numberOfFailedRequests; 2519 | 2520 | setTimeout(() => next(), cached ? 0 : delay); 2521 | } 2522 | } 2523 | ); 2524 | }, 2525 | 1 2526 | ); 2527 | 2528 | function inventoryPriceQueueWorker(item, ignoreErrors, callback) { 2529 | let failed = 0; 2530 | const itemName = item.name || item.description.name; 2531 | 2532 | // Only get the market orders here, the history is not important to visualize the current prices. 2533 | market.getItemOrdersHistogram( 2534 | item, 2535 | true, 2536 | (err, histogram, cachedListings) => { 2537 | if (err) { 2538 | logConsole(`Failed to get orders histogram for ${itemName}`); 2539 | 2540 | if (err != ERROR_SUCCESS) { 2541 | failed += 1; 2542 | } 2543 | } 2544 | 2545 | if (failed > 0 && !ignoreErrors) { 2546 | return callback(false, cachedListings); 2547 | } 2548 | 2549 | const sellPrice = calculateSellPriceBeforeFees(null, histogram, false, 0, 65535); 2550 | 2551 | const itemPrice = sellPrice == 65535 2552 | ? '∞' 2553 | : formatPrice(market.getPriceIncludingFees(sellPrice)); 2554 | 2555 | const elementName = `${(currentPage == PAGE_TRADEOFFER ? '#item' : '#')}${item.appid}_${item.contextid}_${item.id}`; 2556 | const element = $(elementName); 2557 | 2558 | $('.inventory_item_price', element).remove(); 2559 | element.append(`${itemPrice}`); 2560 | 2561 | return callback(true, cachedListings); 2562 | } 2563 | ); 2564 | } 2565 | } 2566 | //#endregion 2567 | 2568 | //#region Market 2569 | if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) { 2570 | const marketListingsRelistedAssets = []; 2571 | let marketProgressBar; 2572 | 2573 | function increaseMarketProgressMax() { 2574 | let value = marketProgressBar.max; 2575 | 2576 | // Reset the progress bar if it already completed 2577 | if (marketProgressBar.value === value) { 2578 | marketProgressBar.value = 0; 2579 | value = 0; 2580 | } 2581 | 2582 | marketProgressBar.max = value + 1; 2583 | marketProgressBar.removeAttribute('hidden'); 2584 | } 2585 | 2586 | function increaseMarketProgress() { 2587 | marketProgressBar.value += 1; 2588 | 2589 | if (marketProgressBar.value === marketProgressBar.max) { 2590 | marketProgressBar.setAttribute('hidden', 'true'); 2591 | } 2592 | } 2593 | 2594 | // Match number part from any currency format 2595 | const getPriceValueAsInt = listing => 2596 | unsafeWindow.GetPriceValueAsInt( 2597 | listing.match(/(?[0-9][0-9 .,]*)/)?.groups?.price ?? 0 2598 | ); 2599 | 2600 | const marketListingsQueue = async.queue((listing, next) => { 2601 | marketListingsQueueWorker( 2602 | listing, 2603 | false, 2604 | (success, cached) => { 2605 | const callback = () => { 2606 | increaseMarketProgress(); 2607 | next(); 2608 | }; 2609 | 2610 | if (success) { 2611 | setTimeout(callback, cached ? 0 : getRandomInt(1000, 1500)); 2612 | } else { 2613 | setTimeout(() => marketListingsQueueWorker(listing, true, callback), cached ? 0 : getRandomInt(30000, 45000)); 2614 | } 2615 | } 2616 | ); 2617 | }, 1); 2618 | 2619 | 2620 | function marketListingsQueueWorker(listing, ignoreErrors, callback) { 2621 | const asset = unsafeWindow.g_rgAssets[listing.appid][listing.contextid][listing.assetid]; 2622 | 2623 | // An asset: 2624 | //{ 2625 | // "currency" : 0, 2626 | // "appid" : 753, 2627 | // "contextid" : "6", 2628 | // "id" : "4363079664", 2629 | // "classid" : "2228526061", 2630 | // "instanceid" : "0", 2631 | // "amount" : "1", 2632 | // "status" : 2, 2633 | // "original_amount" : "1", 2634 | // "background_color" : "", 2635 | // "icon_url" : "xx", 2636 | // "icon_url_large" : "xxx", 2637 | // "descriptions" : [{ 2638 | // "value" : "Their dense, shaggy fur conceals the presence of swams of moogamites, purple scaly skin, and more nipples than one would expect." 2639 | // } 2640 | // ], 2641 | // "tradable" : 1, 2642 | // "owner_actions" : [{ 2643 | // "link" : "http://steamcommunity.com/my/gamecards/443880/", 2644 | // "name" : "View badge progress" 2645 | // }, { 2646 | // "link" : "javascript:GetGooValue( '%contextid%', '%assetid%', 443880, 7, 0 )", 2647 | // "name" : "Turn into Gems..." 2648 | // } 2649 | // ], 2650 | // "name" : "Wook", 2651 | // "type" : "Loot Rascals Trading Card", 2652 | // "market_name" : "Wook", 2653 | // "market_hash_name" : "443880-Wook", 2654 | // "market_fee_app" : 443880, 2655 | // "commodity" : 1, 2656 | // "market_tradable_restriction" : 7, 2657 | // "market_marketable_restriction" : 7, 2658 | // "marketable" : 1, 2659 | // "app_icon" : "xxxx", 2660 | // "owner" : 0 2661 | //} 2662 | 2663 | const market_hash_name = getMarketHashName(asset); 2664 | const appid = listing.appid; 2665 | 2666 | const listingUI = $(getListingFromLists(listing.listingid).elm); 2667 | 2668 | const game_name = asset.type; 2669 | const price = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listingUI).text()); 2670 | 2671 | if (price <= getSettingWithDefault(SETTING_PRICE_MIN_CHECK_PRICE) * 100) { 2672 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_NOT_CHECKED); 2673 | $('.market_listing_my_price', listingUI).last().prop('title', 'The price is not checked.'); 2674 | listingUI.addClass('not_checked'); 2675 | 2676 | return callback(true, true); 2677 | } 2678 | 2679 | const priceInfo = getPriceInformationFromItem(asset); 2680 | const item = { 2681 | appid: parseInt(appid), 2682 | description: { 2683 | market_hash_name: market_hash_name 2684 | } 2685 | }; 2686 | 2687 | let failed = 0; 2688 | 2689 | market.getPriceHistory( 2690 | item, 2691 | true, 2692 | (errorPriceHistory, history, cachedHistory) => { 2693 | if (errorPriceHistory) { 2694 | logConsole(`Failed to get price history for ${game_name}`); 2695 | 2696 | if (errorPriceHistory != ERROR_SUCCESS) { 2697 | failed += 1; 2698 | } 2699 | } 2700 | 2701 | market.getItemOrdersHistogram( 2702 | item, 2703 | true, 2704 | (errorHistogram, histogram, cachedListings) => { 2705 | if (errorHistogram) { 2706 | logConsole(`Failed to get orders histogram for ${game_name}`); 2707 | 2708 | if (errorHistogram != ERROR_SUCCESS) { 2709 | failed += 1; 2710 | } 2711 | } 2712 | 2713 | if (failed > 0 && !ignoreErrors) { 2714 | return callback(false, cachedHistory && cachedListings); 2715 | } 2716 | 2717 | // Shows the highest buy order price on the market listings. 2718 | // The 'histogram.highest_buy_order' is not reliable as Steam is caching this value, but it gives some idea for older titles/listings. 2719 | const highestBuyOrderPrice = histogram == null || histogram.highest_buy_order == null 2720 | ? '-' 2721 | : formatPrice(histogram.highest_buy_order); 2722 | $( 2723 | '.market_table_value > span:nth-child(1) > span:nth-child(1) > span:nth-child(1)', 2724 | listingUI 2725 | ).append(` ➤ ${highestBuyOrderPrice 2726 | }`); 2727 | 2728 | logConsole('============================'); 2729 | logConsole(JSON.stringify(listing)); 2730 | logConsole(`${game_name}: ${asset.name}`); 2731 | logConsole(`Current price: ${price / 100.0}`); 2732 | 2733 | // Calculate two prices here, one without the offset and one with the offset. 2734 | // 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). 2735 | // The price with the offset should be used for relisting so it will still apply the user-set offset. 2736 | 2737 | const sellPriceWithoutOffset = calculateSellPriceBeforeFees( 2738 | history, 2739 | histogram, 2740 | false, 2741 | priceInfo.minPriceBeforeFees, 2742 | priceInfo.maxPriceBeforeFees 2743 | ); 2744 | const sellPriceWithOffset = calculateSellPriceBeforeFees( 2745 | history, 2746 | histogram, 2747 | true, 2748 | priceInfo.minPriceBeforeFees, 2749 | priceInfo.maxPriceBeforeFees 2750 | ); 2751 | 2752 | const sellPriceWithoutOffsetWithFees = market.getPriceIncludingFees(sellPriceWithoutOffset); 2753 | 2754 | logConsole(`Calculated price: ${sellPriceWithoutOffsetWithFees / 100.0} (${sellPriceWithoutOffset / 100.0})`); 2755 | 2756 | listingUI.addClass(`price_${sellPriceWithOffset}`); 2757 | 2758 | $('.market_listing_my_price', listingUI).last().prop( 2759 | 'title', 2760 | `The best price is ${formatPrice(sellPriceWithoutOffsetWithFees)}.` 2761 | ); 2762 | 2763 | if (sellPriceWithoutOffsetWithFees < price) { 2764 | logConsole('Sell price is too high.'); 2765 | 2766 | $('.market_listing_my_price', listingUI).last(). 2767 | css('background', COLOR_PRICE_EXPENSIVE); 2768 | listingUI.addClass('overpriced'); 2769 | 2770 | if (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1) { 2771 | queueOverpricedItemListing(listing.listingid); 2772 | } 2773 | } else if (sellPriceWithoutOffsetWithFees > price) { 2774 | logConsole('Sell price is too low.'); 2775 | 2776 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_CHEAP); 2777 | listingUI.addClass('underpriced'); 2778 | } else { 2779 | logConsole('Sell price is fair.'); 2780 | 2781 | $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_FAIR); 2782 | listingUI.addClass('fair'); 2783 | } 2784 | 2785 | return callback(true, cachedHistory && cachedListings); 2786 | } 2787 | ); 2788 | } 2789 | ); 2790 | } 2791 | 2792 | const marketOverpricedQueue = async.queue( 2793 | (item, next) => { 2794 | marketOverpricedQueueWorker( 2795 | item, 2796 | false, 2797 | (success) => { 2798 | const callback = () => { 2799 | increaseMarketProgress(); 2800 | next(); 2801 | }; 2802 | 2803 | if (success) { 2804 | setTimeout(callback, getRandomInt(1000, 1500)); 2805 | } else { 2806 | setTimeout(() => marketOverpricedQueueWorker(item, true, callback), getRandomInt(30000, 45000)); 2807 | } 2808 | } 2809 | ); 2810 | }, 2811 | 1 2812 | ); 2813 | 2814 | function marketOverpricedQueueWorker(item, ignoreErrors, callback) { 2815 | const listingUI = getListingFromLists(item.listing).elm; 2816 | 2817 | market.removeListing( 2818 | item.listing, false, 2819 | (errorRemove) => { 2820 | if (!errorRemove) { 2821 | $('.actual_content', listingUI).css('background', COLOR_PENDING); 2822 | 2823 | setTimeout(() => { 2824 | const itemName = $('.market_listing_item_name_link', listingUI).first().attr('href'); 2825 | const marketHashNameIndex = itemName.lastIndexOf('/') + 1; 2826 | const marketHashName = itemName.substring(marketHashNameIndex); 2827 | const decodedMarketHashName = decodeURIComponent(itemName.substring(marketHashNameIndex)); 2828 | let newAssetId = -1; 2829 | 2830 | unsafeWindow.RequestFullInventory(`${market.inventoryUrl + item.appid}/${item.contextid}/`, {}, null, null, (transport) => { 2831 | if (transport.responseJSON && transport.responseJSON.success) { 2832 | const inventory = transport.responseJSON.rgInventory; 2833 | 2834 | for (const child in inventory) { 2835 | if (marketListingsRelistedAssets.indexOf(child) == -1 && inventory[child].appid == item.appid && (inventory[child].market_hash_name == decodedMarketHashName || inventory[child].market_hash_name == marketHashName)) { 2836 | newAssetId = child; 2837 | break; 2838 | } 2839 | } 2840 | 2841 | if (newAssetId == -1) { 2842 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2843 | return callback(false); 2844 | } 2845 | 2846 | item.assetid = newAssetId; 2847 | marketListingsRelistedAssets.push(newAssetId); 2848 | 2849 | market.sellItem( 2850 | item, 2851 | item.sellPrice, 2852 | (errorSell) => { 2853 | if (!errorSell) { 2854 | $('.actual_content', listingUI).css('background', COLOR_SUCCESS); 2855 | 2856 | setTimeout(() => { 2857 | removeListingFromLists(item.listing); 2858 | }, 3000); 2859 | 2860 | return callback(true); 2861 | } else { 2862 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2863 | return callback(false); 2864 | } 2865 | } 2866 | ); 2867 | 2868 | } else { 2869 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2870 | return callback(false); 2871 | } 2872 | }); 2873 | }, getRandomInt(1500, 2500)); // Wait a little to make sure the item is returned to inventory. 2874 | } else { 2875 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2876 | return callback(false); 2877 | } 2878 | } 2879 | ); 2880 | } 2881 | 2882 | // Queue an overpriced item listing to be relisted. 2883 | function queueOverpricedItemListing(listingid) { 2884 | const assetInfo = getAssetInfoFromListingId(listingid); 2885 | const listingUI = $(getListingFromLists(listingid).elm); 2886 | let price = -1; 2887 | 2888 | const items = $(listingUI).attr('class').split(' '); 2889 | for (const i in items) { 2890 | if (items[i].toString().includes('price_')) { 2891 | price = parseInt(items[i].toString().replace('price_', '')); 2892 | } 2893 | } 2894 | 2895 | if (price > 0) { 2896 | marketOverpricedQueue.push({ 2897 | listing: listingid, 2898 | assetid: assetInfo.assetid, 2899 | contextid: assetInfo.contextid, 2900 | appid: assetInfo.appid, 2901 | sellPrice: price 2902 | }); 2903 | increaseMarketProgressMax(); 2904 | } 2905 | } 2906 | 2907 | const marketRemoveQueue = async.queue( 2908 | (listingid, next) => { 2909 | marketRemoveQueueWorker( 2910 | listingid, 2911 | false, 2912 | (success) => { 2913 | const callback = () => { 2914 | increaseMarketProgress(); 2915 | next(); 2916 | }; 2917 | 2918 | if (success) { 2919 | setTimeout(callback, getRandomInt(50, 100)); 2920 | } else { 2921 | setTimeout(() => marketRemoveQueueWorker(listingid, true, callback), getRandomInt(30000, 45000)); 2922 | } 2923 | } 2924 | ); 2925 | }, 2926 | 1 2927 | ); 2928 | 2929 | function marketRemoveQueueWorker(listingid, ignoreErrors, callback) { 2930 | const listingUI = getListingFromLists(listingid).elm; 2931 | const isBuyOrder = listingUI.id.startsWith('mybuyorder_'); 2932 | 2933 | market.removeListing( 2934 | listingid, isBuyOrder, 2935 | (errorRemove) => { 2936 | if (!errorRemove) { 2937 | $('.actual_content', listingUI).css('background', COLOR_SUCCESS); 2938 | 2939 | setTimeout( 2940 | () => { 2941 | removeListingFromLists(listingid); 2942 | 2943 | const numberOfListings = marketLists[0].size; 2944 | if (numberOfListings > 0) { 2945 | $('#my_market_selllistings_number').text(numberOfListings.toString()); 2946 | 2947 | // This seems identical to the number of sell listings. 2948 | $('#my_market_activelistings_number').text(numberOfListings.toString()); 2949 | } 2950 | }, 2951 | 3000 2952 | ); 2953 | 2954 | return callback(true); 2955 | } else { 2956 | $('.actual_content', listingUI).css('background', COLOR_ERROR); 2957 | 2958 | return callback(false); 2959 | } 2960 | } 2961 | ); 2962 | } 2963 | 2964 | const marketListingsItemsQueue = async.queue( 2965 | (listing, next) => { 2966 | const callback = () => { 2967 | increaseMarketProgress(); 2968 | setTimeout(() => next(), getRandomInt(1000, 1500)); 2969 | }; 2970 | 2971 | const url = `${window.location.origin}/market/mylistings` 2972 | 2973 | const options = { 2974 | method: 'GET', 2975 | data: { 2976 | count: 100, 2977 | start: listing 2978 | }, 2979 | responseType: 'json' 2980 | }; 2981 | 2982 | request( 2983 | url, 2984 | options, 2985 | (error, data) => { 2986 | if (error || !data?.success) { 2987 | callback(); 2988 | return; 2989 | } 2990 | 2991 | const myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); 2992 | 2993 | const nodes = $.parseHTML(data.results_html); 2994 | const rows = $('.market_listing_row', nodes); 2995 | myMarketListings.append(rows); 2996 | 2997 | // g_rgAssets 2998 | unsafeWindow.MergeWithAssetArray(data.assets); // This is a method from Steam. 2999 | 3000 | callback(); 3001 | } 3002 | ) 3003 | }, 3004 | 1 3005 | ); 3006 | 3007 | marketListingsItemsQueue.drain(() => { 3008 | const myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); 3009 | myMarketListings.checkboxes('range', true); 3010 | 3011 | // Sometimes the Steam API is returning duplicate entries (especially during item listing), filter these. 3012 | const seen = {}; 3013 | $('.market_listing_row', myMarketListings).each(function () { 3014 | const item_id = $(this).attr('id'); 3015 | if (seen[item_id]) { 3016 | $(this).remove(); 3017 | } else { 3018 | seen[item_id] = true; 3019 | } 3020 | 3021 | // Remove listings awaiting confirmations, they are already listed separately. 3022 | if ($('.item_market_action_button', this).attr('href').toLowerCase(). 3023 | includes('CancelMarketListingConfirmation'.toLowerCase())) { 3024 | $(this).remove(); 3025 | } 3026 | 3027 | // Remove buy order listings, they are already listed separately. 3028 | if ($('.item_market_action_button', this).attr('href').toLowerCase(). 3029 | includes('CancelMarketBuyOrder'.toLowerCase())) { 3030 | $(this).remove(); 3031 | } 3032 | }); 3033 | 3034 | // Now add the market checkboxes. 3035 | addMarketCheckboxes(); 3036 | 3037 | // Show the listings again, rendering is done. 3038 | $('#market_listings_spinner').remove(); 3039 | myMarketListings.show(); 3040 | 3041 | fillMarketListingsQueue(); 3042 | }); 3043 | 3044 | function fillMarketListingsQueue() { 3045 | $('.market_home_listing_table').each(function (e) { 3046 | 3047 | // Not for popular / new / recently sold items (bottom of page). 3048 | if ($('.my_market_header', $(this)).length == 0) { 3049 | return; 3050 | } 3051 | 3052 | // Buy orders and listings confirmations are not grouped like the sell listings, add this so pagination works there as well. 3053 | if (!$(this).attr('id')) { 3054 | $(this).attr('id', `market-listing-${e}`); 3055 | 3056 | $(this).append(`
`); 3057 | $('.market_listing_row', $(this)).appendTo($(`#market-listing-container-${e}`)); 3058 | } else { 3059 | $(this).children().last().addClass('market_listing_see'); 3060 | } 3061 | 3062 | const marketListing = $('.market_listing_see', this).last(); 3063 | if (marketListing[0].childElementCount > 0) { 3064 | addMarketListings(marketListing); 3065 | sortMarketListings($(this), false, false, true); 3066 | } 3067 | }); 3068 | 3069 | let totalPriceBuyer = 0; 3070 | let totalPriceSeller = 0; 3071 | let totalAmount = 0; 3072 | 3073 | // Add the listings to the queue to be checked for the price. 3074 | marketLists.flatMap(list => list.items).forEach(item => { 3075 | const listingid = replaceNonNumbers(item.values().market_listing_item_name); 3076 | const assetInfo = getAssetInfoFromListingId(listingid); 3077 | 3078 | if (assetInfo.appid === undefined) { 3079 | logConsole(`Skipping listing ${listingid} (no sell order)`); 3080 | return; 3081 | } 3082 | 3083 | totalAmount += assetInfo.amount; 3084 | 3085 | if (!isNaN(assetInfo.priceBuyer)) { 3086 | totalPriceBuyer += assetInfo.priceBuyer * assetInfo.amount; 3087 | } 3088 | if (!isNaN(assetInfo.priceSeller)) { 3089 | totalPriceSeller += assetInfo.priceSeller * assetInfo.amount; 3090 | } 3091 | 3092 | marketListingsQueue.push({ 3093 | listingid, 3094 | appid: assetInfo.appid, 3095 | contextid: assetInfo.contextid, 3096 | assetid: assetInfo.assetid 3097 | }); 3098 | increaseMarketProgressMax(); 3099 | }); 3100 | 3101 | $('#my_market_selllistings_number').append(` [${totalAmount}]`) 3102 | .append(`, ${formatPrice(totalPriceBuyer)} ➤ ${formatPrice(totalPriceSeller)}`); 3103 | } 3104 | 3105 | 3106 | // Gets the asset info (appid/contextid/assetid) based on a listingid. 3107 | function getAssetInfoFromListingId(listingid) { 3108 | const listing = getListingFromLists(listingid); 3109 | if (listing == null) { 3110 | return {}; 3111 | } 3112 | 3113 | const actionButton = $('.item_market_action_button', listing.elm).attr('href'); 3114 | // Market buy orders have no asset info. 3115 | if (actionButton == null || actionButton.toLowerCase().includes('cancelmarketbuyorder')) { 3116 | return {}; 3117 | } 3118 | 3119 | const priceBuyer = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listing.elm).text()); 3120 | const priceSeller = getPriceValueAsInt($('.market_listing_price > span:nth-child(1) > span:nth-child(3)', listing.elm).text()); 3121 | const itemIds = actionButton.split(','); 3122 | const appid = replaceNonNumbers(itemIds[2]); 3123 | const contextid = replaceNonNumbers(itemIds[3]); 3124 | const assetid = replaceNonNumbers(itemIds[4]); 3125 | const amount = Number(unsafeWindow.g_rgAssets[appid][contextid][assetid]?.amount ?? 1); 3126 | return { 3127 | appid, 3128 | contextid, 3129 | assetid, 3130 | amount, 3131 | priceBuyer, 3132 | priceSeller 3133 | }; 3134 | } 3135 | 3136 | // Adds market item listings. 3137 | function addMarketListings(market_listing_see) { 3138 | market_listing_see.addClass('list'); 3139 | 3140 | $('.market_listing_table_header', market_listing_see.parent()). 3141 | append(''); 3142 | 3143 | const options = { 3144 | valueNames: [ 3145 | 'market_listing_game_name', 3146 | 'market_listing_item_name_link', 3147 | 'market_listing_price', 3148 | 'market_listing_listed_date', 3149 | { 3150 | name: 'market_listing_item_name', 3151 | attr: 'id' 3152 | } 3153 | ] 3154 | }; 3155 | 3156 | try { 3157 | const list = new List(market_listing_see.parent().get(0), options); 3158 | list.on('searchComplete', updateMarketSelectAllButton); 3159 | marketLists.push(list); 3160 | } catch (e) { 3161 | console.error(e); 3162 | } 3163 | } 3164 | 3165 | // Adds checkboxes to market listings. 3166 | function addMarketCheckboxes() { 3167 | $('.market_listing_row').each(function () { 3168 | // Don't add it again, one time is enough. 3169 | if ($('.market_listing_select', this).length == 0) { 3170 | $('.market_listing_cancel_button', $(this)).append('
' + 3171 | '' + 3172 | '
'); 3173 | 3174 | $('.market_select_item', this).change(() => { 3175 | updateMarketSelectAllButton(); 3176 | }); 3177 | } 3178 | }); 3179 | } 3180 | 3181 | // Process the market listings. 3182 | function processMarketListings() { 3183 | addMarketCheckboxes(); 3184 | 3185 | if (currentPage == PAGE_MARKET) { 3186 | // Load the market listings. 3187 | let currentCount = 0; 3188 | let totalCount = 0; 3189 | 3190 | if (typeof unsafeWindow.g_oMyListings !== 'undefined' && unsafeWindow.g_oMyListings != null && unsafeWindow.g_oMyListings.m_cTotalCount != null) { 3191 | totalCount = unsafeWindow.g_oMyListings.m_cTotalCount; 3192 | } else { 3193 | totalCount = parseInt($('#my_market_selllistings_number').text()); 3194 | } 3195 | 3196 | if (isNaN(totalCount) || totalCount == 0) { 3197 | fillMarketListingsQueue(); 3198 | return; 3199 | } 3200 | 3201 | $('#tabContentsMyActiveMarketListingsRows').html(''); // Clear the default listings. 3202 | $('#tabContentsMyActiveMarketListingsRows').hide(); // Hide all listings until everything has been loaded. 3203 | 3204 | // Hide Steam's paging controls. 3205 | $('#tabContentsMyActiveMarketListings_ctn').hide(); 3206 | $('.market_pagesize_options').hide(); 3207 | 3208 | // Show the spinner so the user knows that something is going on. 3209 | $('.my_market_header').eq(0).append(`
${spinnerBlock 3210 | }
Loading market listings
` + 3211 | '
'); 3212 | 3213 | while (currentCount < totalCount) { 3214 | marketListingsItemsQueue.push(currentCount); 3215 | increaseMarketProgressMax(); 3216 | currentCount += 100; 3217 | } 3218 | } else { 3219 | // This is on a market item page. 3220 | $('.market_home_listing_table').each(function () { 3221 | // Not on 'x requests to buy at y,yy or lower'. 3222 | if ($('#market_buyorder_info_show_details', $(this)).length > 0) { 3223 | return; 3224 | } 3225 | 3226 | $(this).children().last().wrap('
'); 3227 | const marketListing = $('.market_listing_see', this).last(); 3228 | 3229 | if (marketListing[0].childElementCount > 0) { 3230 | addMarketListings(marketListing); 3231 | sortMarketListings($(this), false, false, true); 3232 | } 3233 | }); 3234 | 3235 | $('#tabContentsMyActiveMarketListingsRows > .market_listing_row').each(function () { 3236 | const listingid = $(this).attr('id').replace('mylisting_', '').replace('mybuyorder_', '').replace('mbuyorder_', ''); 3237 | const assetInfo = getAssetInfoFromListingId(listingid); 3238 | 3239 | // There's only one item in the g_rgAssets on a market listing page. 3240 | let existingAsset = null; 3241 | for (const appid in unsafeWindow.g_rgAssets) { 3242 | for (const contextid in unsafeWindow.g_rgAssets[appid]) { 3243 | for (const assetid in unsafeWindow.g_rgAssets[appid][contextid]) { 3244 | existingAsset = unsafeWindow.g_rgAssets[appid][contextid][assetid]; 3245 | break; 3246 | } 3247 | } 3248 | } 3249 | 3250 | // appid and contextid are identical, only the assetid is different for each asset. 3251 | unsafeWindow.g_rgAssets[assetInfo.appid][assetInfo.contextid][assetInfo.assetid] = existingAsset; 3252 | marketListingsQueue.push({ 3253 | listingid, 3254 | appid: assetInfo.appid, 3255 | contextid: assetInfo.contextid, 3256 | assetid: assetInfo.assetid 3257 | }); 3258 | increaseMarketProgressMax(); 3259 | }); 3260 | } 3261 | } 3262 | 3263 | // Update the select/deselect all button on the market. 3264 | function updateMarketSelectAllButton() { 3265 | $('.market_listing_buttons').each(function () { 3266 | const selectionGroup = $(this).parent().parent(); 3267 | let invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length; 3268 | if ($('.market_select_item', selectionGroup).length == 0) { // If there are no items to select, keep it at Select all. 3269 | invert = false; 3270 | } 3271 | $('.select_all > span', selectionGroup).text(invert ? 'Deselect all' : 'Select all'); 3272 | }); 3273 | } 3274 | 3275 | // Sort the market listings. 3276 | function sortMarketListings(elem, isPrice, isDateOrQuantity, isName) { 3277 | const list = getListFromContainer(elem); 3278 | if (list == null) { 3279 | console.log('Invalid parameter, could not find a list matching elem.'); 3280 | return; 3281 | } 3282 | 3283 | // Change sort order (asc/desc). 3284 | let asc = true; 3285 | 3286 | // (Re)set the asc/desc arrows. 3287 | const arrow_down = '🡻'; 3288 | const arrow_up = '🡹'; 3289 | 3290 | $('.market_listing_table_header > span', elem).each(function () { 3291 | if ($(this).hasClass('market_listing_edit_buttons')) { 3292 | return; 3293 | } 3294 | 3295 | if ($(this).text().includes(arrow_up)) { 3296 | asc = false; 3297 | } 3298 | 3299 | $(this).text($(this).text().replace(` ${arrow_down}`, '').replace(` ${arrow_up}`, '')); 3300 | }); 3301 | 3302 | let market_listing_selector; 3303 | if (isPrice) { 3304 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(1); 3305 | } else if (isDateOrQuantity) { 3306 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(2); 3307 | } else if (isName) { 3308 | market_listing_selector = $('.market_listing_table_header', elem).children().eq(3); 3309 | } 3310 | market_listing_selector.text(`${market_listing_selector.text()} ${asc ? arrow_up : arrow_down}`); 3311 | 3312 | if (list.sort == null) { 3313 | return; 3314 | } 3315 | 3316 | const isBuyOrder = list.list.querySelectorAll('.market_listing_buyorder_qty').length >= 1; 3317 | 3318 | if (isName) { 3319 | list.sort('', { 3320 | order: asc ? 'asc' : 'desc', 3321 | sortFunction: function (a, b) { 3322 | if (a.values().market_listing_game_name.toLowerCase(). 3323 | localeCompare(b.values().market_listing_game_name.toLowerCase()) == 3324 | 0) { 3325 | return a.values().market_listing_item_name_link.toLowerCase(). 3326 | localeCompare(b.values().market_listing_item_name_link.toLowerCase()); 3327 | } 3328 | return a.values().market_listing_game_name.toLowerCase(). 3329 | localeCompare(b.values().market_listing_game_name.toLowerCase()); 3330 | } 3331 | }); 3332 | } else if (isDateOrQuantity) { 3333 | const currentMonth = luxon.DateTime.local().month; 3334 | 3335 | if (isBuyOrder) { 3336 | list.sort('market_listing_buyorder_qty', { 3337 | order: asc ? 'asc' : 'desc', 3338 | sortFunction: function (a, b) { 3339 | const quantityA = a.elm.querySelector('.market_listing_buyorder_qty').innerText; 3340 | const quantityB = b.elm.querySelector('.market_listing_buyorder_qty').innerText; 3341 | 3342 | return quantityA - quantityB; 3343 | } 3344 | }); 3345 | } else { 3346 | list.sort('market_listing_listed_date', { 3347 | order: asc ? 'asc' : 'desc', 3348 | sortFunction: function (a, b) { 3349 | let firstDate = luxon.DateTime.fromString(a.values().market_listing_listed_date.trim(), 'd MMM'); 3350 | let secondDate = luxon.DateTime.fromString(b.values().market_listing_listed_date.trim(), 'd MMM'); 3351 | 3352 | if (firstDate == null || secondDate == null) { 3353 | return 0; 3354 | } 3355 | 3356 | if (firstDate.month > currentMonth) { 3357 | firstDate = firstDate.plus({ years: -1 }); 3358 | } 3359 | if (secondDate.month > currentMonth) { 3360 | secondDate = secondDate.plus({ years: -1 }); 3361 | } 3362 | 3363 | if (firstDate > secondDate) { 3364 | return 1; 3365 | } 3366 | if (firstDate === secondDate) { 3367 | return 0; 3368 | } 3369 | return -1; 3370 | } 3371 | }); 3372 | } 3373 | } else if (isPrice) { 3374 | list.sort('market_listing_price', { 3375 | order: asc ? 'asc' : 'desc', 3376 | sortFunction: function (a, b) { 3377 | if (!isBuyOrder) { 3378 | let listingPriceA = $(a.values().market_listing_price).text(); 3379 | listingPriceA = listingPriceA.substr(0, listingPriceA.indexOf('(')); 3380 | 3381 | let listingPriceB = $(b.values().market_listing_price).text(); 3382 | listingPriceB = listingPriceB.substr(0, listingPriceB.indexOf('(')); 3383 | 3384 | const firstPrice = getPriceValueAsInt(listingPriceA); 3385 | const secondPrice = getPriceValueAsInt(listingPriceB); 3386 | 3387 | return firstPrice - secondPrice; 3388 | } else { 3389 | const priceA = getPriceValueAsInt(a.elm.querySelector('div:nth-child(3) > span:nth-child(1) > span:nth-child(1)').innerText); 3390 | const priceB = getPriceValueAsInt(b.elm.querySelector('div:nth-child(3) > span:nth-child(1) > span:nth-child(1)').innerText); 3391 | 3392 | return priceA - priceB; 3393 | } 3394 | } 3395 | }); 3396 | } 3397 | } 3398 | 3399 | function getListFromContainer(group) { 3400 | for (let i = 0; i < marketLists.length; i++) { 3401 | if (group.attr('id') == $(marketLists[i].listContainer).attr('id')) { 3402 | return marketLists[i]; 3403 | } 3404 | } 3405 | } 3406 | 3407 | function getListingFromLists(listingid) { 3408 | // Sometimes listing ids are contained in multiple lists (?), use the last one available as this is the one we're most likely interested in. 3409 | for (let i = marketLists.length - 1; i >= 0; i--) { 3410 | let values = marketLists[i].get('market_listing_item_name', `mylisting_${listingid}_name`); 3411 | if (values != null && values.length > 0) { 3412 | return values[0]; 3413 | } 3414 | 3415 | values = marketLists[i].get('market_listing_item_name', `mbuyorder_${listingid}_name`); 3416 | if (values != null && values.length > 0) { 3417 | return values[0]; 3418 | } 3419 | } 3420 | 3421 | 3422 | } 3423 | 3424 | function removeListingFromLists(listingid) { 3425 | for (let i = 0; i < marketLists.length; i++) { 3426 | marketLists[i].remove('market_listing_item_name', `mylisting_${listingid}_name`); 3427 | marketLists[i].remove('market_listing_item_name', `mbuyorder_${listingid}_name`); 3428 | } 3429 | } 3430 | 3431 | // Initialize the market UI. 3432 | function initializeMarketUI() { 3433 | $('.market_header_text').append('