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