您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Keep track of your market buy/sale history for Dead Frontier to instantly see your profit and losses
当前为
// ==UserScript== // @name Market History // @namespace http://tampermonkey.net/ // @version 1.9.2 // @description Keep track of your market buy/sale history for Dead Frontier to instantly see your profit and losses // @author Runonstof // @match *fairview.deadfrontier.com/onlinezombiemmo/index.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=deadfrontier.com // @grant unsafeWindow // @grant GM.getValue // @grant GM.setValue // @license MIT // ==/UserScript== (async function() { 'use strict'; /****************************************************** * Initialize script ******************************************************/ const searchParams = new URLSearchParams(window.location.search); const page = parseInt(searchParams.get('page')); // If is not on the market page or yard page, stop script if (![35, 24].includes(page)) { return; } /** * Detect if SilverScripts is installed * * We only need to know this to render hover item info differently * Because if SilverScripts is installed and its HoverPrices are enabled * The hover info box can get cluttered and overflows, causing data to be hidden (Found out during testing) * So if this is detected, a setting will appear to show our data only when the shift key is pressed */ const silverScriptsInstalled = unsafeWindow.hasOwnProperty('silverRequestItem'); /* === Global variables === */ unsafeWindow.historyScreen = 'list'; // 'list', 'stats' unsafeWindow.historyScreenSet = false; // Will be set after page init (below script) // Because DeadFrontier does a call to stackables.json, we need to wait for that to complete let SEARCHABLE_ITEMS = []; const TIMEFRAME_OPTIONS = { all: 'All time', last_24hr: 'Last 24 hours', last_week: 'Last week', last_month: 'Last month', last_3_months: 'Last 3 months', last_6_months: 'Last 6 months', last_year: 'Last year', ytd: 'Since january 1st', mtd: 'Since 1st of month', wtd: 'Since monday', }; const SHIFT_HOVER_OPTIONS = { disabled: 'Disabled', // Just show everything history: 'Enabled', // Show history info when shift is pressed }; if (silverScriptsInstalled) { SHIFT_HOVER_OPTIONS.history = 'History'; // Only show history info when shift is pressed SHIFT_HOVER_OPTIONS.silverscripts = 'SilverScripts'; // Show SilverScripts HoverPrices when shift is pressed } const WEBCALL_HOOKS = { before: {}, after: {}, afterAll: [], beforeAll: [], lastExecutedAt: {}, }; const LOOKUP = { category__item_id: {}, }; // Our history object // That is responsible for keeping track of all trades // And calculating statistics const _HISTORY = { entries: [], selectedItem: null, filters: { minDate: null, maxDate: null, type: 'all', }, getFilteredEntries() { let historyEntries = this.entries; if (this.selectedItem || this.filters.minDate || this.filters.maxDate || this.filters.type !== 'all') { historyEntries = historyEntries.filter(entry => { // Check by item id if (this.selectedItem && (!entry.item || getBaseItemId(entry.item) != HISTORY.selectedItem)) { return false; } // Check by action type if (this.filters.type === 'buy' && entry.action !== 'buy') { return false; } if (this.filters.type === 'sell' && entry.action !== 'sell') { return false; } if (this.filters.type === 'scrap' && entry.action !== 'scrap') { return false; } if (this.filters.type === 'sell_scrap' && entry.action !== 'sell' && entry.action !== 'scrap') { return false; } // Check by date if (this.filters.minDate && entry.date < this.filters.minDate) { return false; } if (this.filters.maxDate) { const checkMaxDate = this.filters.maxDate + 86400000; // Add 1 day if (entry.date > checkMaxDate) { return false; } } return true; }); } return historyEntries; }, // Cached values, to prevent having to loop through all entries every time // Causing performance to improve cache: {}, resetCache() { this.cache = { trade_id: {}, // trades indexed by trade_id item_id: {}, // trades indexed by item_id item_id__amount_sold: {}, // total sell numbers, indexed by item_id item_id__amount_bought: {}, // total buy numbers, indexed by item_id item_id__avg_price_sold: {}, // average sell price, indexed by item_id item_id__avg_price_bought: {}, // average buy price, indexed by item_id item_id__total_price_sold: {}, // total sell price, indexed by item_id item_id__total_price_bought: {}, // total buy price, indexed by item_id item_id__last_price_sold: {}, // last sell price, indexed by item_id item_id__last_price_bought: {}, // last buy price, indexed by item_id item_id__last_quantity_sold: {}, // last sell quantity, indexed by item_id item_id__last_quantity_bought: {}, // last buy quantity, indexed by item_id item_id__last_date_sold: {}, // last sell date, indexed by item_id item_id__last_date_bought: {}, // last buy date, indexed by item_id item_id__sold: {}, // trades indexed by trade_id item_id__bought: {}, // trades indexed by trade_id item_id__scrapped: {}, // trades indexed by trade_id pending_trade_ids: [], // trade_ids of pending trades }; this.initCache(); }, storageKey(key) { return 'HISTORY_' + key + '_' + unsafeWindow.userVars.userID; }, initCache() { const entries = this.entries; for (const entry of entries) { const tradeId = entry.trade_id; const itemId = getBaseItemId(entry.item); // const globalItemId = getGlobalDataItemId(entry.item); this.cache.trade_id[tradeId] = entry; if (!this.cache.item_id.hasOwnProperty(itemId)) { this.cache.item_id[itemId] = []; } this.cache.item_id[itemId].push(entry); // if (!this.cache.item_id__amount_sold.hasOwnProperty(itemId)) { // this.cache.item_id__amount_sold[itemId] = 0; // } // if (!this.cache.item_id__amount_bought.hasOwnProperty(itemId)) { // this.cache.item_id__amount_bought[itemId] = 0; // } // const action = entry.action; // 'buy' or 'sell' // const itemcat = unsafeWindow.globalData[globalItemId].itemcat; // const quantity = realQuantity(entry.quantity, itemcat) // if (action === 'buy') { // this.cache.item_id__amount_bought[itemId] += quantity; // } else if (action === 'sell') { // this.cache.item_id__amount_sold[itemId] += quantity; // } } }, clearCacheForItem(itemId) { delete this.cache.item_id__avg_price_sold[itemId]; delete this.cache.item_id__avg_price_bought[itemId]; delete this.cache.item_id__last_price_sold[itemId]; delete this.cache.item_id__last_price_bought[itemId]; delete this.cache.item_id__last_quantity_sold[itemId]; delete this.cache.item_id__last_quantity_bought[itemId]; delete this.cache.item_id__last_date_sold[itemId]; delete this.cache.item_id__last_date_bought[itemId]; }, getTrade(tradeId) { if (this.cache.item_id.hasOwnProperty(tradeId)) { return this.cache.item_id[tradeId]; } return this.cache.item_id[tradeId] = this.entries.find(entry => entry.trade_id === tradeId);; }, async pushTrade(entry) { if (!entry.date) { entry.date = Date.now(); } await this.load(); this.entries.push(entry); await this.forceSave(); this.cache.trade_id[entry.trade_id] = entry; const itemId = getBaseItemId(entry.item); // Update cache if (!this.cache.item_id.hasOwnProperty(itemId)) { this.cache.item_id[itemId] = []; } this.cache.item_id[itemId].push(entry); // Init amount sold and amount bought cache if (!this.cache.item_id__amount_sold.hasOwnProperty(itemId)) { this.cache.item_id__amount_sold[itemId] = 0; } if (!this.cache.item_id__amount_bought.hasOwnProperty(itemId)) { this.cache.item_id__amount_bought[itemId] = 0; } // Add to quantity cache const action = entry.action; // 'buy' or 'sell' const quantity = parseInt(entry.quantity); if (action === 'buy') { // TODO: check date treshold? this.cache.item_id__amount_bought[itemId] += quantity; } else if (action === 'sell') { // TODO: check date treshold? this.cache.item_id__amount_sold[itemId] += quantity; } this.clearCacheForItem(itemId); }, async sortEntries() { await this.load(); this.entries.sort((a, b) => (a.date > b.date) ? 1 : -1); await this.forceSave(); }, // Remove trade from history async removeTrade(tradeId, isIndex = false) { await this.load(); let index = isIndex ? tradeId : this.entries.findIndex(entry => entry.trade_id === tradeId); if (isIndex) { if (index < 0 || index >= this.entries.length) { return false; } } if (index == -1) { return false; } const entry = this.entries[index]; this.entries.splice(index, 1); await this.forceSave(); // Update cache delete this.cache.trade_id[tradeId]; // Remove from item_id cache const itemId = getBaseItemId(entry.item); if (this.cache.item_id.hasOwnProperty(itemId)) { const itemIndex = this.cache.item_id[itemId].findIndex(entry => entry.trade_id === tradeId); if (itemIndex > -1) { this.cache.item_id[itemId].splice(itemIndex, 1); } } const quantity = entry.quantity; const action = entry.action; // 'buy' or 'sell' if (action === 'buy' && this.cache.item_id__amount_bought.hasOwnProperty(itemId)) { this.cache.item_id__amount_bought[itemId] -= quantity; } else if (action === 'sell' && this.cache.item_id__amount_sold.hasOwnProperty(itemId)) { this.cache.item_id__amount_sold[itemId] -= quantity; } const pendingTradeIndex = this.cache.pending_trade_ids.indexOf(tradeId); if (pendingTradeIndex > -1) { this.cache.pending_trade_ids.splice(pendingTradeIndex, 1); } this.clearCacheForItem(itemId); return true; }, // Get info about an item, based on its trades // Looks up the cache first, if not found, calculates it getItemInfo(itemId, key, timeframe = undefined) { let cacheKey = 'item_id__' + key; let trades, total, action, amount, lastTradeId, lastTradePrice, lastTradeQuantity, lastTradeDate; const isDateInTreshold = function (entry) { const entryDate = entry.date; const tresholdDate = getTresholdDateForTimeframe(SETTINGS.values.hoverStatisticsTimeframe); if (tresholdDate === null) { return true; } return entryDate >= tresholdDate.getTime(); } switch (key) { case 'amount_sold': case 'amount_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } trades = this.cache.item_id[itemId] || []; action = key == 'amount_sold' ? 'sell' : 'buy'; return this.cache[cacheKey][itemId] = trades.reduce((total, trade) => { if (!isDateInTreshold(trade)) { return total; } let isAction = trade.action === action; if (!isAction && SETTINGS.values.countScraps && action == 'sell') { isAction = trade.action === 'scrap'; } if (!isAction) { return total; } const tradeItemId = getGlobalDataItemId(trade.item); const quantity = realQuantity(trade.quantity, unsafeWindow.globalData[tradeItemId].itemcat); return total + quantity; }, 0); break; case 'last_price_sold': case 'last_price_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } trades = this.cache.item_id[itemId] || []; action = key == 'last_price_sold' ? 'sell' : 'buy'; lastTradeId = 0; lastTradeDate = 0; lastTradePrice = 0; let tradeCount = 0; for (const trade of trades) { if (!isDateInTreshold(trade)) { continue; } let isAction = trade.action === action; if (!isAction && SETTINGS.values.countScraps && action == 'sell') { isAction = trade.action === 'scrap'; } if (!isAction) { continue; } if (trade.item !== itemId) { continue; } if (trade.date <= lastTradeDate) { continue; } tradeCount++; lastTradeId = trade.trade_id; lastTradePrice = trade.price; lastTradeDate = trade.date; } if (tradeCount === 0) { return this.cache[cacheKey][itemId] = null; } return this.cache[cacheKey][itemId] = lastTradePrice; break; case 'last_quantity_sold': case 'last_quantity_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } trades = this.cache.item_id[itemId] || []; action = key == 'last_quantity_sold' ? 'sell' : 'buy'; lastTradeId = 0; lastTradeQuantity = 0; for (const trade of trades) { if (!isDateInTreshold(trade)) { continue; } if (trade.action !== action) { continue; } if (trade.date <= lastTradeDate) { continue; } const tradeItemId = getGlobalDataItemId(trade.item); lastTradeId = trade.trade_id; lastTradeQuantity = realQuantity(trade.quantity, unsafeWindow.globalData[tradeItemId].itemcat); lastTradeDate = trade.date; } return this.cache[cacheKey][itemId] = lastTradeQuantity; break; case 'last_date_sold': case 'last_date_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } trades = this.cache.item_id[itemId] || []; action = key == 'last_date_sold' ? 'sell' : 'buy'; lastTradeId = 0; lastTradeDate = 0; for (const trade of trades) { if (!isDateInTreshold(trade)) { continue; } if (trade.action !== action) { continue; } if (trade.date <= lastTradeDate) { continue; } lastTradeId = trade.trade_id; lastTradeDate = trade.date; } return this.cache[cacheKey][itemId] = lastTradeDate; break; case 'avg_price_sold': case 'avg_price_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } amount = this.getItemInfo(itemId, key.replace('avg_price', 'amount')); // amount_sold or amount_bought if (amount == 0) { return this.cache[cacheKey][itemId] = 0; } action = (key == 'avg_price_sold' ? 'sell' : 'buy'); trades = this.cache.item_id[itemId]; total = this.getItemInfo(itemId, key.replace('avg_price', 'total_price')); // total_price_sold or total_price_bought return this.cache[cacheKey][itemId] = total / amount; break; case 'total_price_sold': case 'total_price_bought': if (this.cache[cacheKey].hasOwnProperty(itemId)) { return this.cache[cacheKey][itemId]; } amount = this.getItemInfo(itemId, key.replace('total_price', 'amount')); // amount_sold or amount_bought if (amount == 0) { return this.cache[cacheKey][itemId] = 0; } action = (key == 'total_price_sold' ? 'sell' : 'buy'); trades = this.cache.item_id[itemId]; total = 0; for (const trade of trades) { if (!isDateInTreshold(trade)) { continue; } let isAction = trade.action === action; if (!isAction && SETTINGS.values.countScraps && action == 'sell') { isAction = trade.action === 'scrap'; } if (!isAction) { continue; } total += parseInt(trade.price); } return this.cache[cacheKey][itemId] = total; break; case 'avg_stack_price_sold': case 'avg_stack_price_bought': const avgPrice = this.getItemInfo(itemId, key.replace('avg_stack_price', 'avg_price')); // avg_price_sold or avg_price_bought // const totalAmount = this.getItemInfo(itemId, key.replace('avg_stack_price', 'amount')); // amount_sold or amount_bought const stack = maxStack(itemId); return avgPrice * stack; break; } throw new Error('Invalid key: ' + key); }, async forceSave() { if (this._debugMode) { return; } await GM.setValue(this.storageKey('entries'), this.entries); }, // Called during debugging async clearEntries() { this.entries = []; await this.forceSave(); }, async setSelectedItem(item) { this.selectedItem = item ? getBaseItemId(item) : null; await GM.setValue(this.storageKey('selectedItem'), item); }, async saveFilters() { await GM.setValue(this.storageKey('filters'), this.filters); }, async init() { this.selectedItem = await GM.getValue(this.storageKey('selectedItem'), null); this.filters = mergeDeep({}, this.filters, await GM.getValue(this.storageKey('filters'), {})); await this.load(); }, async load() { if (this._debugMode) { return; } this.entries = await GM.getValue(this.storageKey('entries'), []); }, _debugMode: false, debugMode(mode = null) { if (mode === null) { this.debugMode(!this._debugMode); return; } this._debugMode = mode; }, renderEntryPrompt(entry) { pageLock = true; unsafeWindow.prompt.classList.remove("warning"); unsafeWindow.prompt.classList.remove("redhighlight"); unsafeWindow.prompt.style.height = "200px"; const imgItemId = getGlobalDataItemId(entry.item); const imgUrl = 'https://files.deadfrontier.com/deadfrontier/inventoryimages/large/' + imgItemId + '.png'; unsafeWindow.prompt.innerHTML = ` <div style="text-align: center; text-decoration: underline;">Edit entry</div> <div style="text-align: right; position: absolute; right: 0;"><img src="${imgUrl}" /></div> <div style="z-index: 1000; position: relative;"> <span style="text-decoration: underline;">Item:</span><br> ${entry.itemname}${entry.item == 'credits' ? '' : ` x${entry.quantity}`} </div> <br> <div style="position: relative;"> <div style="position: absolute;"> <span style="text-decoration: underline;">Action:</span><br> <span style="color: #00FF00;">${entry.action}</span> </div> <div style="position: absolute; left: 100px;"> <span style="text-decoration: underline;">Price:</span><br> ${formatMoneyHtml(entry.price, true)} </div> <br> <br> </div> <br> <div> <span style="text-decoration: underline;">Datetime:</span><br> ${formatDate(new Date(entry.date))} </div> `; const historyEntryHolder = document.createElement("div"); historyEntryHolder.id = "historyEntryHolder"; // const footerButton = document.createElement("button"); // footerButton.style.position = "absolute"; // footerButton.style.bottom = "12px"; // if (footerButtonInfo.action) { // footerButton.addEventListener("click", footerButtonInfo.action); // } // footerButton.textContent = footerButtonInfo.label; // for(const styleKey in footerButtonInfo.style) { // footerButton.style[styleKey] = footerButtonInfo.style[styleKey]; // } // unsafeWindow.prompt.appendChild(footerButton); const closeBtn = document.createElement("button"); closeBtn.style.position = "absolute"; closeBtn.style.bottom = "12px"; closeBtn.style.right = "12px"; closeBtn.textContent = "close"; closeBtn.addEventListener("click", () => { HISTORY.closeEntryPrompt(); }); unsafeWindow.prompt.appendChild(closeBtn); const itemInfoBtn = document.createElement("button"); itemInfoBtn.style.position = "absolute"; itemInfoBtn.style.bottom = "12px"; itemInfoBtn.style.left = "100px"; itemInfoBtn.textContent = "item stats"; itemInfoBtn.addEventListener("click", () => { HISTORY.closeEntryPrompt(); unsafeWindow.historyScreen = 'stats'; HISTORY.setSelectedItem(entry.item); loadMarket(); }); unsafeWindow.prompt.appendChild(itemInfoBtn); const removeBtn = document.createElement("button"); removeBtn.style.position = "absolute"; removeBtn.style.bottom = "12px"; removeBtn.style.left = "12px"; removeBtn.textContent = "remove"; removeBtn.addEventListener("click", async () => { const confirmed = confirm('Are you sure you want to remove this entry?'); if (!confirmed) { return; } unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>"; const removed = await HISTORY.removeTrade(entry.trade_id); if (!removed) { alert('Could not remove entry'); return; } HISTORY.closeEntryPrompt(); // HISTORY.resetCache(); loadMarket(); }); unsafeWindow.prompt.appendChild(removeBtn); const editBtn = document.createElement("button"); editBtn.style.position = "absolute"; editBtn.style.bottom = "30px"; editBtn.style.left = "12px"; editBtn.textContent = "edit"; editBtn.addEventListener("click", async () => { HISTORY.renderEntryFormPrompt(entry); // const confirmed = confirm('Are you sure you want to remove this entry?'); // if (!confirmed) { // return; // } // const removed = await HISTORY.removeTrade(entry.trade_id); // if (!removed) { // alert('Could not remove entry'); // return; // } // HISTORY.closeEntryPrompt(); // // HISTORY.resetCache(); // loadMarket(); }); unsafeWindow.prompt.appendChild(editBtn); unsafeWindow.prompt.parentNode.style.display = "block"; unsafeWindow.prompt.focus(); }, renderEntryFormPrompt(entry) { pageLock = true; unsafeWindow.prompt.classList.remove("warning"); unsafeWindow.prompt.classList.remove("redhighlight"); unsafeWindow.prompt.style.height = "200px"; const imgItemId = getGlobalDataItemId(entry.item); const imgUrl = 'https://files.deadfrontier.com/deadfrontier/inventoryimages/large/' + imgItemId + '.png'; const itemData = unsafeWindow.globalData[imgItemId]; if (!entry.itemname) { entry.itemname = unsafeWindow.itemNamer(entry.item, ''); } const maxQuantity = maxStack(entry.item, false); unsafeWindow.prompt.innerHTML = ` <div class="historyEntryForm"> <div style="text-align: center; text-decoration: underline;">${entry.trade_id ? 'Edit' : 'Create'} entry</div> <div style="text-align: right; position: absolute; right: 0;"><img src="${imgUrl}" /></div> <div style="z-index: 1000; position: relative;"> <span style="text-decoration: underline;">Item:</span><br> ${entry.itemname} <br> <input type="number" min="0" max="${maxQuantity}" placeholder="Quantity" style="width: 50px;" id="entryFormQuantity" value="${entry.quantity || maxQuantity}" /> </div> <br> <div style="position: relative;"> <div style="position: absolute;"> <span style="text-decoration: underline;">Action:</span><br> <div data-value="${entry.action || 'buy'}" id="entryFormAction" class="historySelectComponent"> <div class="selectChoice"> <span></span> <span></span> </div> <div class="selectList"> <div data-value="buy" class="selectOption">Buy</div> <div data-value="sell" class="selectOption">Sell</div> <div data-value="scrap" class="selectOption">Scrap</div> </div> </div> </div> <div style="position: absolute; left: 100px;"> <span style="text-decoration: underline;">Price:</span><br> <span style="color: #FFCC00;">$</span> <input type="number" min="1" max="9999999999" placeholder="Price" style="width: 50px;" id="entryFormPrice" value="${entry.price || 0}" /> </div> <br> <br> </div> <br> <div> <span style="text-decoration: underline;">Datetime:</span><br> <input type="datetime-local" id="entryFormDate" value="${formatDate(new Date(entry.date || Date.now()))}" /> </div> </div> `; initHistorySelects(); const historyEntryHolder = document.createElement("div"); historyEntryHolder.id = "historyEntryHolder"; // const footerButton = document.createElement("button"); // footerButton.style.position = "absolute"; // footerButton.style.bottom = "12px"; // if (footerButtonInfo.action) { // footerButton.addEventListener("click", footerButtonInfo.action); // } // footerButton.textContent = footerButtonInfo.label; // for(const styleKey in footerButtonInfo.style) { // footerButton.style[styleKey] = footerButtonInfo.style[styleKey]; // } // unsafeWindow.prompt.appendChild(footerButton); const closeBtn = document.createElement("button"); closeBtn.style.position = "absolute"; closeBtn.style.bottom = "12px"; closeBtn.style.right = "12px"; closeBtn.textContent = "cancel"; closeBtn.addEventListener("click", () => { HISTORY.closeEntryPrompt(); }); unsafeWindow.prompt.appendChild(closeBtn); // const itemInfoBtn = document.createElement("button"); // itemInfoBtn.style.position = "absolute"; // itemInfoBtn.style.bottom = "12px"; // itemInfoBtn.style.left = "100px"; // itemInfoBtn.textContent = "item stats"; // itemInfoBtn.addEventListener("click", () => { // HISTORY.closeEntryPrompt(); // unsafeWindow.historyScreen = 'stats'; // HISTORY.setSelectedItem(entry.item); // loadMarket(); // }); // unsafeWindow.prompt.appendChild(itemInfoBtn); const saveBtn = document.createElement("button"); saveBtn.style.position = "absolute"; saveBtn.style.bottom = "12px"; saveBtn.style.left = "12px"; saveBtn.textContent = entry.trade_id ? "save" : "add"; saveBtn.addEventListener("click", async () => { if (saveBtn.disabled) { return; } // validate and parse values const quantity = parseInt(document.getElementById('entryFormQuantity').value); const price = parseInt(document.getElementById('entryFormPrice').value); const date = new Date(document.getElementById('entryFormDate').value); const action = document.getElementById('entryFormAction').dataset.value; const maxQuantity = maxStack(entry.item, true); if (isNaN(quantity) || quantity < 1 || quantity > maxQuantity) { alert('Invalid quantity, max is ' + maxQuantity); return; } if (isNaN(price) || price < 0 || price > 9999999999) { alert('Invalid price'); return; } if (isNaN(date.getTime())) { alert('Invalid date'); return; } if (entry.trade_id) { const confirmed = confirm('Are you sure you want to overwrite this entry?'); if (!confirmed) { return; } saveBtn.disabled = true; const newEntryData = { quantity, price, date: date.getTime(), action, }; if (entry.item == 'credits') { entry.itemname = unsafeWindow.itemNamer(entry.item, quantity); } const newEntry = mergeDeep({}, entry, newEntryData); const removed = await HISTORY.removeTrade(entry.trade_id); if (!removed) { alert('Could not remove entry'); return; } unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>"; await HISTORY.pushTrade(newEntry); } else { saveBtn.disabled = true; const newEntry = { trade_id: uniqid(16), item: entry.item, itemname: unsafeWindow.itemNamer(entry.item, quantity), quantity, price, date: date.getTime(), action, }; unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>"; await HISTORY.pushTrade(newEntry); } await HISTORY.sortEntries(); HISTORY.closeEntryPrompt(); loadMarket(); }); unsafeWindow.prompt.appendChild(saveBtn); // const editBtn = document.createElement("button"); // editBtn.style.position = "absolute"; // editBtn.style.bottom = "30px"; // editBtn.style.left = "12px"; // editBtn.textContent = "edit"; // editBtn.addEventListener("click", async () => { // // const confirmed = confirm('Are you sure you want to remove this entry?'); // // if (!confirmed) { // // return; // // } // // const removed = await HISTORY.removeTrade(entry.trade_id); // // if (!removed) { // // alert('Could not remove entry'); // // return; // // } // // HISTORY.closeEntryPrompt(); // // // HISTORY.resetCache(); // // loadMarket(); // }); // unsafeWindow.prompt.appendChild(editBtn); unsafeWindow.prompt.parentNode.style.display = "block"; unsafeWindow.prompt.focus(); }, closeEntryPrompt() { pageLock = false; unsafeWindow.prompt.parentNode.style.display = "none"; unsafeWindow.prompt.innerHTML = ""; unsafeWindow.prompt.classList.remove("warning"); unsafeWindow.prompt.classList.remove("redhighlight"); }, /** * Executed when an item is sold, and then the new sell listing is retrieved * The response contains the trade id, which is what we need to keep track of the trade */ onSellItem(request, response) { // console.log('trying to push sell trade: ', JSON.stringify(response.dataObj, null, 4)); // console.log('Share this info with Runon if needed'); const tradeCount = response.dataObj.tradelist_totalsales; if (tradeCount == 0) { return; } let recentTrade = null; const props = [ 'category', 'deny_private', 'id_member', 'id_member_to', 'item', 'itemname', 'member_name', 'member_to_name', 'price', 'pricerper', 'quantity', 'trade_id', 'trade_zone', ]; for(let i = 0; i < tradeCount; i++) { const tradeId = response.dataObj['tradelist_' + i + '_trade_id']; if (recentTrade && recentTrade.trade_id >= tradeId) { continue; } const entry = { action: 'sell', }; for (const prop of props) { entry[prop] = response.dataObj['tradelist_' + i + '_' + prop]; } recentTrade = entry; } if (!recentTrade) { return; } this.pushTrade(recentTrade); }, }; const HISTORY = new Proxy(_HISTORY, { get(target, prop) { return Reflect.get(...arguments); }, set(target, prop, value) { return Reflect.set(...arguments); }, }); const SETTINGS = { ui: { main: { title: 'History Menu', text: 'Welcome to History Help and Settings!', elements: { settings: { type: 'button', title: 'Settings', action() { SETTINGS.renderSettingsPrompt('settings'); } }, help: { type: 'button', title: 'Help', action() { SETTINGS.renderSettingsPrompt('help'); } }, export: { type: 'button', title: 'Export', action() { SETTINGS.renderSettingsPrompt('export'); } }, actions: { type: 'button', title: 'Actions', action() { SETTINGS.renderSettingsPrompt('actions'); } }, credits: { type: 'button', title: 'Credits', action() { SETTINGS.renderSettingsPrompt('credits'); } }, }, footerButtons: [ { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, help: { text: 'This script keeps track of all your market trades and your item scraps, and calculates statistics like average sell price.<br><br><span style="color: #FF0000">NOTE:</span> Your history is saved into TamperMonkey data, so if you log in on another computer, your history will not be available there.<br><br>Use the export function to export your history', title: 'Help', elements: {}, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('main'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, export: { title: 'Export', text: 'Export your history to a csv file', elements: { exportSortBy: { title: 'Sort by', type: 'switch', description: 'The column to sort by', options: { name: 'Item name', quantity: 'Quantity', price: 'Price', action: 'Trade type', date: 'Date', }, }, exportSortDirection: { title: 'Sort direction', type: 'switch', description: 'The direction to sort by', options: { asc: 'Ascending', desc: 'Descending', }, }, exportTimeframe: { title: 'Export timeframe', // type: 'timeframeselect', type: 'switch', description: 'The timeframe that will be used to export the history.', options: TIMEFRAME_OPTIONS }, download: { type: 'button', title: 'Download export', description: 'Starts the exports and downloads the csv file', async action() { await HISTORY.load(); const sortBy = SETTINGS.values.exportSortBy; const sortDirection = SETTINGS.values.exportSortDirection; const timeframe = SETTINGS.values.exportTimeframe; const tresholdDate = getTresholdDateForTimeframe(timeframe); let filteredEntries = HISTORY.entries.filter(entry => { const entryDate = new Date(entry.date); if (tresholdDate && entryDate < tresholdDate) { return false; } return true; }); const sortByKey = sortBy == 'name' ? 'item' : sortBy; const sortDirectionMultiplier = sortDirection == 'asc' ? 1 : -1; const sortStrategy = ['price', 'quantity'].includes(sortByKey) ? 'numeric' : 'string'; filteredEntries.sort((a, b) => { const aKey = a[sortByKey]; const bKey = b[sortByKey]; if (sortStrategy == 'numeric') { return (aKey - bKey) * sortDirectionMultiplier; } if (sortStrategy == 'string') { if (aKey < bKey) { return -1 * sortDirectionMultiplier; } if (aKey > bKey) { return 1 * sortDirectionMultiplier; } return 0; } throw new Error('Invalid sort strategy: ' + sortStrategy); }); filteredEntries = filteredEntries.map(entry => [ entry.trade_id, entry.item, entry.itemname, entry.quantity, entry.price, entry.action, formatDate(new Date(entry.date)), ]); // Insert header at top filteredEntries.unshift(['id', 'item', 'name', 'quantity', 'price', 'action', 'date']); exportToCsv('market_trades_tracker_export.csv', filteredEntries, SETTINGS.values.exportSeperator); } } }, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('main'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, actions: { title: 'Actions', text: '', elements: { clear: { type: 'button', title: 'Clear history', description: 'Clears all market sell/buy/scrap history older than a specific timeframe<br><br><span style="color: #FF0000">WARNING:</span> This cannot be undone!', action() { SETTINGS.renderSettingsPrompt('clear_confirm'); } }, clearCache: { type: 'button', title: 'Clear cache', description: 'Clears the cache, which will cause the script to recalculate all statistics', action() { HISTORY.resetCache(); alert('Cache cleared'); } }, }, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('main'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, clear_confirm: { class: 'warning', title: 'Clear history', text: 'Are you really sure you want to clear history?<br><br>You can delete single entries by clicking on them in the history tab.<br><br><span style="color: #FF0000">WARNING:</span> This cannot be undone!', descriptionTop: '200px', elements: { clearHistoryTimeframe: { title: 'Timeframe', type: 'switch', description: 'History entries that are older than this timeframe will be deleted.', options: TIMEFRAME_OPTIONS }, }, footerButtons: [ { label: 'no', action() { SETTINGS.renderSettingsPrompt('actions'); }, style: { left: '12px', } }, { label: 'yes', async action() { if (SETTINGS.values.clearHistoryTimeframe == 'all') { await HISTORY.clearEntries(); } else { const tresholdDate = getTresholdDateForTimeframe(SETTINGS.values.clearHistoryTimeframe); await HISTORY.load(); const entries = HISTORY.entries.filter(entry => { const entryDate = new Date(entry.date); return entryDate >= tresholdDate; }); HISTORY.entries = entries; await HISTORY.forceSave(); } HISTORY.resetCache(); SETTINGS.closePrompt(); window.location.reload(); }, style: { right: '12px', } } ] }, credits: { title: 'Credits', text: 'This script was made by <span style="color: #FF0000">Runonstof</span>. If you have any questions or suggestions, feel free to contact me on Discord: <span style="color: #FF0000">runon</span>', elements: { donate: { type: 'button', title: 'Donate', description: 'Redirects to my profile page, where you can donate anything if you want to support me.', action() { window.location.href = '/onlinezombiemmo/index.php?action=profile;u=12925065'; } } }, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('main'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, settings: { title: 'Settings', text: '', elements: { hoverSettings: { type: 'button', title: '>> Hover info settings', description: 'Settings for the hover info module.', action() { SETTINGS.renderSettingsPrompt('hoverSettings'); } }, // hoverAvgPriceEnabled: { // title: 'Avg price hover enabled', // description: 'Show average sell/buy price and profit/loss in set timeframe on item hover', // type: 'checkbox', // }, // hoverLastPriceEnabled: { // title: 'Last price hover enabled', // description: 'Show last sell/buy price and date on item hover', // type: 'checkbox', // }, // autoFillBreakEvenPrice: { // title: 'Auto fill price', // type: 'checkbox', // description: 'When selling an item, the price will be automatically filled in with the average sell price in the set timeframe.', // }, countPendingTrades: { title: 'Calculate with pending', type: 'checkbox', meta: { resetCache: true, }, description: 'When calculating statistics, pending trades will be taken into account, if changed, you might have to reload statistics screen.', }, countScraps: { title: 'Calculate with scraps', type: 'checkbox', meta: { resetCache: true, }, description: 'When calculating statistics, scraps will be taken into account, if changed, you might have to reload statistics screen.', }, hoverStatisticsTimeframe: { title: 'Timeframe', // type: 'timeframeselect', type: 'switch', description: 'The timeframe that will be used to calculate statistics.', options: { all: 'All time', last_24hr: 'Last 24 hours', last_week: 'Last week', last_month: 'Last month', last_3_months: 'Last 3 months', last_6_months: 'Last 6 months', last_year: 'Last year', ytd: 'Since january 1st', mtd: 'Since 1st of month', }, meta: { resetCache: true, }, }, defaultHistoryPage: { title: 'Default page', type: 'switch', description: 'The page that will be shown by default when opening the history tab.', options: { list: 'List', stats: 'Statistics', }, }, }, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('main'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, hoverSettings: { title: 'Hover settings', text: 'Here you can configure what elements you see when you hover an item.', descriptionStrategy: 'text', style: { position: 'absolute', bottom: '50px', }, elements: { hoverEnabled: { title: 'Hover info enabled', description: 'Show info on item hover', type: 'checkbox', }, hoverAvgSellPriceEnabled: { title: 'Average sell price', description: 'Show average sell price in set timeframe.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverAvgBuyPriceEnabled: { title: 'Average buy price', description: 'Show average buy price in set timeframe.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverAmountSoldEnabled: { title: 'Amount sold', description: 'Show amount sold in set timeframe.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverAmountBoughtEnabled: { title: 'Amount bought', description: 'Show amount bought in set timeframe.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverLastSellPriceEnabled: { title: 'Last sell price', description: 'Show the most recent price you sold this item for.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverLastBuyPriceEnabled: { title: 'Last buy price', description: 'Show the most recent price you bought this item for.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; }, }, hoverAvgProfitEnabled: { title: 'Average profit per item', description: 'Show average profit per item in set timeframe.', type: 'checkbox', disabled() { return !SETTINGS.values.hoverEnabled; } }, shiftHoverMode: { title: 'Hold shift mode', type: 'switch', description() { if (SETTINGS.values.shiftHoverMode == 'disabled') { return 'Shift hover is disabled' + (silverScriptsInstalled ? ', both SilverScripts and History Data are shown without holding SHIFT.' : ', History Data is shown without holding SHIFT.'); } else if (SETTINGS.values.shiftHoverMode == 'history') { return 'When SHIFT is held, History Data will only be shown' + (silverScriptsInstalled ? ', otherwise SilverScripts\' HoverPrices are shown.' : '.'); } else if (SETTINGS.values.shiftHoverMode == 'silverscripts') { return 'When SHIFT is held, SilverScripts\' HoverPrices are only shown, otherwise History Data is shown.'; } return ''; }, disabled() { return !SETTINGS.values.hoverEnabled; }, options: SHIFT_HOVER_OPTIONS, }, }, footerButtons: [ { label: 'back', action() { SETTINGS.renderSettingsPrompt('settings'); }, style: { left: '12px', } }, { label: 'close', action() { SETTINGS.closePrompt(); }, style: { right: '12px', } } ], }, }, // Seperate values object, where settings are loaded one by one, for if i decide to add settings later on values: { // If true, sell/buy statistics will be shown in the item tooltip when an item is hovered hoverEnabled: true, hoverAvgSellPriceEnabled: true, hoverAvgBuyPriceEnabled: true, hoverAmountSoldEnabled: true, hoverAmountBoughtEnabled: true, hoverLastSellPriceEnabled: true, hoverLastBuyPriceEnabled: true, hoverAvgProfitEnabled: true, shiftHoverMode: silverScriptsInstalled ? 'history' : 'disabled', // 'disabled', 'history', 'silverscripts' defaultHistoryPage: 'list', hoverStatisticsTimeframe: 'all', clearHistoryTimeframe: 'all', // If true, the script will automatically fill in the price when selling an item // The price will be the average buy price of the item of the configured timeframe autoFillBreakEvenPrice: true, // If true, the script will take trades that are still pending into account when calculating statistics countPendingTrades: true, countScraps: true, exportSortBy: 'date', exportSortDirection: 'asc', exportTimeframe: 'all', exportSeperator: ';', }, async reset() { await GM.setValue('SETTINGS_values', {}); this.values = {}; await this.load(); }, async load() { const values = await GM.getValue('SETTINGS_values', {}); // Merge values with default values this.values = mergeDeep(this.values, values); if (!silverScriptsInstalled && this.values.shiftHoverMode == 'silverscripts') { this.values.shiftHoverMode = 'disabled'; } if (!unsafeWindow.historyScreenSet) { unsafeWindow.historyScreen = this.values.defaultHistoryPage; unsafeWindow.historyScreenSet = true; } }, async save() { await GM.setValue('SETTINGS_values', this.values); }, async toggle(setting) { await this.load(); this.values[setting] = !this.values[setting]; await this.save(); }, async set(setting, value) { await this.load(); this.values[setting] = value; await this.save(); }, closePrompt() { unsafeWindow.prompt.parentNode.style.display = "none"; unsafeWindow.prompt.innerHTML = ""; unsafeWindow.prompt.style.height = ""; pageLock = false; unsafeWindow.prompt.classList.remove("warning"); unsafeWindow.prompt.classList.remove("redhighlight"); console.log('reloading market:' + unsafeWindow.marketScreen + ' ' + unsafeWindow.historyScreen); if (unsafeWindow.marketScreen == 'history' && unsafeWindow.historyScreen == 'stats') { unsafeWindow.loadMarket(); } }, renderSettingsPrompt(page = 'main') { pageLock = true; unsafeWindow.prompt.classList.remove("warning"); unsafeWindow.prompt.classList.remove("redhighlight"); const pageInfo = this.ui[page]; if (pageInfo.class) { unsafeWindow.prompt.classList.add(pageInfo.class); } unsafeWindow.prompt.style.height = "280px"; unsafeWindow.prompt.innerHTML = pageInfo.title ? '<div style="text-align: center; text-decoration: underline">' + pageInfo.title + '</div>' : ''; if (pageInfo.text) { unsafeWindow.prompt.innerHTML += '<div id="historySettingsPageText">' + pageInfo.text + '</div>'; } unsafeWindow.prompt.innerHTML += '<br />'; const historySettingsHolder = document.createElement("div"); historySettingsHolder.id = "historySettingsHolder"; // historySettingsHolder.style.position = "absolute"; this._renderUi(historySettingsHolder, page); unsafeWindow.prompt.appendChild(historySettingsHolder); for(const footerButtonInfo of pageInfo.footerButtons || []) { const footerButton = document.createElement("button"); footerButton.style.position = "absolute"; footerButton.style.bottom = "12px"; if (footerButtonInfo.action) { footerButton.addEventListener("click", footerButtonInfo.action); } footerButton.textContent = footerButtonInfo.label; for(const styleKey in footerButtonInfo.style) { footerButton.style[styleKey] = footerButtonInfo.style[styleKey]; } unsafeWindow.prompt.appendChild(footerButton); } unsafeWindow.prompt.parentNode.style.display = "block"; unsafeWindow.prompt.focus(); }, _renderDescription(holder, descriptionText, pageInfo) { const strategy = pageInfo.descriptionStrategy || 'bottom'; if (typeof descriptionText === 'function') { descriptionText = descriptionText(); } if (strategy == 'bottom') { const descriptionElement = document.getElementById('historySettingsDescription'); // delete the element if (descriptionElement) { descriptionElement.parentNode.removeChild(descriptionElement); } if (!descriptionText) { return; } const description = document.createElement('div'); description.id = 'historySettingsDescription'; description.innerHTML = descriptionText; description.style.position = 'absolute'; const top = pageInfo.descriptionTop || '140px'; description.style.top = top; holder.appendChild(description); } else if (strategy == 'text') { const historySettingsPageText = document.getElementById('historySettingsPageText'); if (!historySettingsPageText) { return; } historySettingsPageText.style.display = 'none'; const existingDescription = document.getElementById('historySettingsDescription'); if (existingDescription) { existingDescription.parentNode.removeChild(existingDescription); } if (!descriptionText) { historySettingsPageText.style.display = ''; return; } const description = document.createElement('div'); description.id = 'historySettingsDescription'; description.innerHTML = descriptionText; historySettingsPageText.parentNode.insertBefore(description, historySettingsPageText.nextSibling); } }, _renderUi(holder, page = 'main') { const pageInfo = this.ui[page]; if (pageInfo.style) { for(const styleKey in pageInfo.style) { holder.style[styleKey] = pageInfo.style[styleKey]; } } const elements = pageInfo.elements; holder.innerHTML = ''; const self = this; for(const settingKey in elements) { const setting = elements[settingKey]; const buttonHolder = document.createElement('div'); switch (setting.type) { case 'paragraph': break; case 'checkbox': const checkbox = document.createElement('button'); checkbox.innerText = '[' + (this.values[settingKey] ? 'x' : ' ') + '] ' + setting.title; if (typeof setting.disabled === 'function') { checkbox.disabled = setting.disabled(); } if (checkbox.disabled) { buttonHolder.appendChild(checkbox); holder.appendChild(buttonHolder); break; } checkbox.addEventListener('click', async () => { await self.toggle(settingKey); if (setting.meta) { if (setting.meta.resetCache) { HISTORY.resetCache(); } } self._renderUi(holder, page); }); if (setting.description) { checkbox.addEventListener('mouseover', function () { self._renderDescription(holder, setting.description, pageInfo); }); checkbox.addEventListener('mouseout', function () { self._renderDescription(holder, null, pageInfo); }); } buttonHolder.appendChild(checkbox); holder.appendChild(buttonHolder); break; case 'switch': const switcher = document.createElement('button'); const settingValue = this.values[settingKey]; const settingValueTitle = setting.options[settingValue]; switcher.innerText = setting.title + ': ' + settingValueTitle; if (typeof setting.disabled === 'function') { switcher.disabled = setting.disabled(); } if (switcher.disabled) { buttonHolder.appendChild(switcher); holder.appendChild(buttonHolder); break; } switcher.addEventListener('click', async e => { const valueKeys = Object.keys(setting.options); const valueIndex = valueKeys.indexOf(settingValue); let nextValueIndex = 0; if (e.shiftKey) { nextValueIndex = valueIndex - 1 < 0 ? valueKeys.length - 1 : valueIndex - 1; } else { nextValueIndex = valueIndex + 1 >= valueKeys.length ? 0 : valueIndex + 1; } const nextValue = valueKeys[nextValueIndex]; await self.set(settingKey, nextValue); if (setting.meta) { if (setting.meta.resetCache) { HISTORY.resetCache(); } } if (typeof setting.afterSaving === 'function') { setting.afterSaving(); } self._renderUi(holder, page); }); if (setting.description) { switcher.addEventListener('mouseover', function () { self._renderDescription(holder, setting.description, pageInfo); }); switcher.addEventListener('mouseout', function () { self._renderDescription(holder, null, pageInfo); }); } buttonHolder.appendChild(switcher); holder.appendChild(buttonHolder); break; case 'button': const button = document.createElement('button'); button.innerText = setting.title; button.addEventListener('click', setting.action); if (setting.description) { button.addEventListener('mouseover', function () { self._renderDescription(holder, setting.description, pageInfo); }); button.addEventListener('mouseout', function () { self._renderDescription(holder, null, pageInfo); }); } buttonHolder.appendChild(button); holder.appendChild(buttonHolder); break; } } } }; const HOVER_INFOBOX_DATA = { event: null, run (shift=true) { if (infoBox.style.visibility == 'hidden') { // console.log('infoBox is hidden'); return; } if (!this.event) { // console.log('no event'); return; } if (!SETTINGS.values.hoverEnabled) { // console.log('hover info disabled'); return; } if (SETTINGS.values.shiftHoverMode == 'disabled') { // console.log('shift hover mode disabled'); return; } Object.defineProperty(this.event, 'shiftKey', { value: shift, writable: false, configurable: true, }); unsafeWindow.infoCard(this.event); }, }; /****************************************************** * Utility functions ******************************************************/ function GM_addStyle(css) { const style = document.getElementById("GM_addStyle_Runon") || (function() { const style = document.createElement('style'); style.type = 'text/css'; style.id = "GM_addStyle_Runon"; document.head.appendChild(style); return style; })(); const sheet = style.sheet; sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length); } function GM_addStyle_object(selector, rules) { const nested = []; let ruleCount = 0; let css = selector + "{"; for (const key in rules) { if (key[0] == '$') { nested.push({selector: key.substr(1).trim(), rules: rules[key]}) continue; } ruleCount++; css += key.replace(/([A-Z])/g, g => `-${g[0].toLowerCase()}`) + ":" + rules[key] + ";"; } css += "}"; if (ruleCount) { GM_addStyle(css); } for(const nestedRules of nested) { const nestedSelector = nestedRules.selector.replace(/\&/g, selector); GM_addStyle_object(nestedSelector, nestedRules.rules); } } function stringExplode(string) { return Object.fromEntries( string.split("&").map((x) => x.split("=")) ); } function realQuantity(quantity, itemcategory) { if (itemcategory == 'armour' || itemcategory == 'weapon') { return 1; } return parseInt(quantity); } function maxStack(itemId, loose = false) { itemId = getGlobalDataItemId(itemId); const itemcat = unsafeWindow.globalData[itemId].itemcat; if (itemcat == 'armour' || itemcat == 'weapon') { return 1; } // TODO: check if doesnt cause unwanted side effects if (itemcat == 'credits') { return loose ? 9999999 : 1; } return unsafeWindow.globalData[itemId].max_quantity; } function uniqid(length = 16) { return window.btoa(Array.from(window.crypto.getRandomValues(new Uint8Array(length * 2))).map((b) => String.fromCharCode(b)).join("")).replace(/[+/]/g, "").substr(0, length); } /** * Simple object check. * @param item * @returns {boolean} */ function isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); } /** * Deep merge two objects. * @param target * @param ...sources */ function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target.hasOwnProperty(key)) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); } // Hook into webCall, after request is done, but before callback is executed function onBeforeWebCall(call, callback) { if (!call) { // If call is not specified, hook into all calls WEBCALL_HOOKS.beforeAll.push(callback); return; } if (!WEBCALL_HOOKS.before.hasOwnProperty(call)) { WEBCALL_HOOKS.before[call] = []; } WEBCALL_HOOKS.before[call].push(callback); } // Remove hook from webCall function offBeforeWebCall(call, callback) { if (!call) { // If call is not specified, remove hook from all calls const index = WEBCALL_HOOKS.beforeAll.indexOf(callback); if (index > -1) { WEBCALL_HOOKS.beforeAll.splice(index, 1); } return; } if (!WEBCALL_HOOKS.before.hasOwnProperty(call)) { return; } const index = WEBCALL_HOOKS.before[call].indexOf(callback); if (index > -1) { WEBCALL_HOOKS.before[call].splice(index, 1); } } // Hook into webCall, after request is done and after callback is executed function onAfterWebCall(call, callback) { if (!call) { // If call is not specified, hook into all calls WEBCALL_HOOKS.afterAll.push(callback); return; } if (!WEBCALL_HOOKS.after.hasOwnProperty(call)) { WEBCALL_HOOKS.after[call] = []; } WEBCALL_HOOKS.after[call].push(callback); } // Remove hook from webCall function offAfterWebCall(call, callback) { if (!call) { // If call is not specified, remove hook from all calls const index = WEBCALL_HOOKS.afterAll.indexOf(callback); if (index > -1) { WEBCALL_HOOKS.afterAll.splice(index, 1); } return; } if (!WEBCALL_HOOKS.after.hasOwnProperty(call)) { return; } const index = WEBCALL_HOOKS.after[call].indexOf(callback); if (index > -1) { WEBCALL_HOOKS.after[call].splice(index, 1); } } function formatDate(date, options = {}) { const { format = 'datetime', } = options; const offset = date.getTimezoneOffset() date = new Date(date.getTime() - (offset*60*1000)) if (format == 'datetime') { return date.toISOString().split('.')[0].replace('T', ' '); } else if (format == 'date') { return date.toISOString().split('T')[0]; } throw new Error('Invalid date format: ' + format); } function formatNumber(num, options = {}) { const { minimumFractionDigits = 0, maximumFractionDigits = 2, } = options; return (new Number(num)) .toLocaleString('en-US', {minimumFractionDigits, maximumFractionDigits}) .replace(/\.0+$/, ''); } function formatMoney(num, options = {}) { if (typeof options == 'boolean') { options = { plus: options }; } const { plus = false, showFree = false, } = options; if (num == 0 && showFree) { return 'Free'; } const plusSign = plus ? '+' : ''; return (num < 0 ? '-' : plusSign) + '$' + formatNumber(Math.abs(num), options); } function formatMoneyHtml(num, options = {}) { if (typeof options == 'boolean') { options = { neutralColor: options, plus: false }; } const { neutralColor = false, } = options; let color = '#FFCC00'; if (!neutralColor) { color = num < 0 ? '#FF0000' : '#00FF00'; } return '<span style="color: ' + color + '">' + formatMoney(num, options) + '</span>'; } function historyAction(e) { var question = false; var action; var extraData = {}; switch(e.target.dataset.action) { case 'switchHistory': unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>"; unsafeWindow.prompt.parentNode.style.display = "block"; unsafeWindow.historyScreen = e.target.dataset.page; loadMarket(); return; break; } } function debouncedItemSearch() { const searchFn = function (query) { query = query.toLowerCase().replace(/[\.\s]/g, '').trim(); if (!query.length) { return []; } return SEARCHABLE_ITEMS .filter(itemId => { const itemName = unsafeWindow.itemNamer(itemId, '') .toLowerCase() .replace(/[\.\s]/g, ''); return itemName.includes(query) || itemId.includes(query); }) .map(item => { return { item, // name: itemNamer(item, maxStack(item)), name: itemNamer(item, ''), } }) .sort((a, b) => { const aName = a.name?.toLowerCase(); const bName = b.name?.toLowerCase(); return aName.localeCompare(bName); }); }; let timeout; return function (query, callback) { clearTimeout(timeout); timeout = setTimeout(() => { callback(searchFn(query)); }, 400); }; } // To be called to check if results box should be hidden function onDocumentClick(e) { const historyItemSearchResultBox = document.getElementById('historyItemSearchResultBox'); if (!historyItemSearchResultBox || (e.target.closest('#historyItemSearchResultBox') || e.target.closest('#historySearchArea input'))) { return; } historyItemSearchResultBox.classList.add('hidden'); } unsafeWindow.addEventListener('click', onDocumentClick); // @see https://stackoverflow.com/a/24922761 function exportToCsv(filename, rows, seperator) { var processRow = function (row) { var finalVal = ''; for (var j = 0; j < row.length; j++) { var innerValue = row[j] === null ? '' : row[j].toString(); if (row[j] instanceof Date) { innerValue = row[j].toLocaleString(); }; var result = innerValue.replace(/"/g, '""'); if (result.search(/("|,|\n|\s)/g) >= 0) result = '"' + result + '"'; if (j > 0) finalVal += seperator; finalVal += result; } return finalVal + '\n'; }; var csvFile = ''; for (var i = 0; i < rows.length; i++) { csvFile += processRow(rows[i]); } var blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' }); if (navigator.msSaveBlob) { // IE 10+ navigator.msSaveBlob(blob, filename); } else { var link = document.createElement("a"); if (link.download !== undefined) { // feature detection // Browsers that support HTML5 download attribute var url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } } function getGlobalDataItemId(rawItemId) { return rawItemId.split('_')[0]; } function getBaseItemId(rawItemId) { return rawItemId.replace(/_stats\d+/, ''); } function getTresholdDateForTimeframe(timeframe) { let tresholdDate = null; switch (timeframe) { case 'last_24hr': tresholdDate = new Date(); tresholdDate.setDate(tresholdDate.getDate() - 1); break; case 'last_week': tresholdDate = new Date(); tresholdDate.setDate(tresholdDate.getDate() - 7); break; case 'last_month': tresholdDate = new Date(); tresholdDate.setMonth(tresholdDate.getMonth() - 1); break; case 'last_3_months': tresholdDate = new Date(); tresholdDate.setMonth(tresholdDate.getMonth() - 3); break; case 'last_6_months': tresholdDate = new Date(); tresholdDate.setMonth(tresholdDate.getMonth() - 6); break; case 'last_year': tresholdDate = new Date(); tresholdDate.setFullYear(tresholdDate.getFullYear() - 1); break; case 'ytd': tresholdDate = new Date(); tresholdDate.setMonth(0); tresholdDate.setDate(1); break; case 'mtd': tresholdDate = new Date(); tresholdDate.setDate(1); break; case 'wtd': tresholdDate = new Date(); tresholdDate.setDate(tresholdDate.getDate() - tresholdDate.getDay()); break; } return tresholdDate; } function parseDateTimeString(value) { const pattern = /^(\d{4})\-(\d{2})\-(\d{2})(?:\s(\d{2}):(\d{2})(?:\:(\d{2}))?)?$/; const match = value.match(pattern); if (!match) { return null; } const year = parseInt(match[1]); const month = parseInt(match[2]) - 1; const day = parseInt(match[3]); const hour = parseInt(match[4]) || 0; const minute = parseInt(match[5]) || 0; const second = parseInt(match[6]) || 0; return new Date(year, month, day, hour, minute, second); } function ready() { return new Promise(resolve => { if (document.readyState === "complete" || document.readyState === "interactive") { resolve(); } else { document.addEventListener("DOMContentLoaded", resolve); } }); } function injectHistoryTabIntoMarketplace() { if (unsafeWindow.document.getElementById('loadHistory')) { return; } const pageNavigation = document.getElementById('selectMarket'); if (!pageNavigation) { return; } // Async context, i dont like nested callbacks, i like async/await (async function () { // Add history button const historyBtn = document.createElement('button'); historyBtn.setAttribute('data-action', 'switchMarket'); historyBtn.setAttribute('data-page', 'history'); historyBtn.setAttribute('id', 'loadHistory'); historyBtn.innerText = 'history'; historyBtn.addEventListener("click", marketAction); pageNavigation.appendChild(historyBtn); switch (unsafeWindow.marketScreen) { case 'history': historyBtn.disabled = true; pageLogo.textContent = "Market History"; const historyNavigation = document.createElement('div'); historyNavigation.id = 'selectHistoryCategory'; pageNavigation.after(historyNavigation); const listBtn = document.createElement('button'); listBtn.setAttribute('data-action', 'switchHistory'); listBtn.setAttribute('data-page', 'list'); listBtn.setAttribute('id', 'historyList'); listBtn.innerText = 'list'; listBtn.addEventListener("click", historyAction); const statsBtn = document.createElement('button'); statsBtn.setAttribute('data-action', 'switchHistory'); statsBtn.setAttribute('data-page', 'stats'); statsBtn.setAttribute('id', 'historyStats'); statsBtn.innerText = 'statistics'; statsBtn.addEventListener("click", historyAction); historyNavigation.appendChild(listBtn); historyNavigation.appendChild(statsBtn); const searchBox = document.createElement("div"); searchBox.id = "historySearchArea"; const filterBox = document.createElement("div"); filterBox.id = "historyFilterArea"; filterBox.innerHTML = ` <div style="position: relative;"> <div class="opElem" id="filterActionTypeWrapper"> <div data-value="${HISTORY.filters.type}" id="filterActionType" class="historySelectComponent"> <div class="selectChoice"> <span></span> <span></span> </div> <div class="selectList"> <div data-value="all" class="selectOption">- All -</div> <div data-value="buy" class="selectOption">Buy</div> <div data-value="sell" class="selectOption">Sell</div> <div data-value="scrap" class="selectOption">Scrap</div> <div data-value="sell_scrap" class="selectOption">Sell/Scrap</div> </div> </div> </div> <input type="date" id="filterMinDate" class="opElem" value="${HISTORY.filters.minDate ? formatDate(new Date(HISTORY.filters.minDate), {format: 'date'}) : ''}"/> <input type="date" id="filterMaxDate" class="opElem" value="${HISTORY.filters.maxDate ? formatDate(new Date(HISTORY.filters.maxDate), {format: 'date'}) : ''}"/> <button id="filterGo" class="opElem">Filter</button> </div> `; initHistorySelects(); let searchInput; if (HISTORY.selectedItem) { const itemName = unsafeWindow.itemNamer(HISTORY.selectedItem, HISTORY.selectedItem == 'credits' ? '' : maxStack(HISTORY.selectedItem)); /*<div style='display: inline-block;' class="itemName cashhack cashhack-relative" data-cash="${itemName}"> </div> */ searchBox.innerHTML = ` <button id="clearHistoryItem">[x]</button> <div class="opElem" id="selectedItemsWrapper"> <div data-value="${HISTORY.selectedItem}" id="selectedItems" class="historySelectComponent"> <div class="selectChoice"> <span></span> <span></span> </div> <div class="selectList"> <div data-value="${HISTORY.selectedItem}" class="selectOption">${itemName}</div> </div> </div> </div> `; const clearHistoryItemBtn = searchBox.querySelector('#clearHistoryItem'); clearHistoryItemBtn.addEventListener('click', function () { HISTORY.setSelectedItem(null); loadMarket(); }); } else { searchBox.innerHTML = ` <div style='text-align: left; width: 185px; display: inline-block;'> <input id='historySearchField' placeholder='Type to search' type='text' name='historySearch' /> </div> `; searchInput = searchBox.querySelector('#historySearchField'); const searchFn = debouncedItemSearch(); searchInput.addEventListener('input', function () { const value = this.value; searchFn(value, function (results) { searchResultBox.innerHTML = ''; for(const result of results) { // const resultRow = document.createElement('div'); const resultButton = document.createElement('button'); resultButton.innerText = result.name; resultButton.style.width = '100%'; resultButton.style.textAlign = 'left'; resultButton.classList.add("fakeItem"); resultButton.setAttribute("data-type", result.item); resultButton.setAttribute("data-quantity", result.item == 'credits' ? '' : maxStack(result.item)); resultButton.addEventListener('click', async function () { this.disabled = true; await HISTORY.setSelectedItem(result.item) searchResultBox.innerHTML = ''; searchResultBox.classList.add('hidden'); loadMarket(); // searchInput.value = result.name; // searchResultBox.innerHTML = ''; }); // resultRow.appendChild(resultButton); // searchResultBox.appendChild(resultRow); searchResultBox.appendChild(resultButton); } if (!value.length) { searchResultBox.classList.add('hidden'); } else { searchResultBox.classList.remove('hidden'); if (!results.length) { const noResults = document.createElement('div'); noResults.innerText = 'No results found'; searchResultBox.appendChild(noResults); } } }); }); searchInput.addEventListener('blur', function (e) { // Check if not focused on result box if (e.relatedTarget && e.relatedTarget.parentNode.id == 'historyItemSearchResultBox') { return; } searchResultBox.classList.add('hidden'); }); searchInput.addEventListener('focus', function () { // If result box has results, show it if (searchResultBox.children.length) { searchResultBox.classList.remove('hidden'); } }); const searchResultBox = document.createElement("div"); searchResultBox.id = "historyItemSearchResultBox"; searchResultBox.classList.add("hidden"); searchBox.appendChild(searchResultBox); } marketHolder.appendChild(searchBox); if (unsafeWindow.historyScreen == 'list') { marketHolder.appendChild(filterBox); unsafeWindow.document.getElementById('filterActionType').oncustomselect = function (event) { if (event.cause == 'init') { return; } HISTORY.filters.type = event.value; // loadMarket(); } unsafeWindow.document.getElementById('filterMinDate').addEventListener('change', function () { // console.log('min date change'); // console.log(this.value); let value = this.value || null; if (value) { // Convert yyyy-mm-dd to timestamp like from Date.now() value = new Date(value).getTime(); } HISTORY.filters.minDate = value; }); unsafeWindow.document.getElementById('filterMaxDate').addEventListener('change', function () { // console.log('max date change'); // console.log(this.value); let value = this.value || null; if (value) { // Convert yyyy-mm-dd to timestamp like from Date.now() value = new Date(value).getTime(); } HISTORY.filters.maxDate = value; }); unsafeWindow.document.getElementById('filterGo').addEventListener('click', async function () { if (this.disabled) { return; } this.disabled = true; await HISTORY.saveFilters(); loadMarket(); }); } initHistorySelects(); // Add history navbar below switch (unsafeWindow.historyScreen) { case 'list': listBtn.disabled = true; let historyEntries = HISTORY.getFilteredEntries(); const boxLabels = document.createElement("div"); boxLabels.id = "historyLabels"; boxLabels.innerHTML = ` <span>Item Name</span> <span style='position: absolute; left: 208px; width: 80px; width: max-content;'>Type</span> <span style='position: absolute; left: 320px; width: max-content;'>Price</span> <span style='position: absolute; left: 480px; width: 70px; width: max-content;'>Datetime</span> `; boxLabels.classList.add("opElem"); boxLabels.style.top = "141px"; boxLabels.style.left = "26px"; const insertBtn = document.createElement("button"); insertBtn.id = "historyInsertBtn"; insertBtn.classList.add("opElem"); insertBtn.style.top = "80px"; insertBtn.style.right = "20px"; insertBtn.innerText = 'Create new entry'; insertBtn.addEventListener('click', function () { if (pageLock) return; if (!HISTORY.selectedItem) { alert('Please select an item first to create an entry of'); searchInput?.focus(); return; } HISTORY.renderEntryFormPrompt({ item: HISTORY.selectedItem, }); }); marketHolder.appendChild(insertBtn); const historyResultsText = document.createElement("div"); historyResultsText.id = "historyResultsText"; historyResultsText.classList.add("opElem"); historyResultsText.style.top = "80px"; historyResultsText.style.left = "20px"; // historyResultsText.style.width = "100%"; historyResultsText.innerText = historyEntries.length + ' result' + (historyEntries.length == 1 ? '' : 's') + ' found'; const historyItemDisplay = document.createElement("div"); historyItemDisplay.id = "historyItemDisplay"; historyItemDisplay.classList.add("marketDataHolder"); historyItemDisplay.setAttribute('data-offset', 0); historyItemDisplay.setAttribute('data-per-page', 20); const renderHistoryItems = function () { const offset = parseInt(historyItemDisplay.getAttribute('data-offset')); const perPage = parseInt(historyItemDisplay.getAttribute('data-per-page')); const entryCount = historyEntries.length; if (offset >= entryCount) { return false; } for(let i = 0; i < perPage; i++) { const entryIndex = entryCount - offset - i - 1; const entry = historyEntries[entryIndex] || null; // const entryIndex = i + offset; // const entry = historyEntries[entryIndex] || null; if (!entry) { continue; } const isPending = HISTORY.cache.pending_trade_ids.includes(entry.trade_id); // if (isPending) { // continue; // } // <div class="fakeItem" data-type="avalanchemg14_stats777" data-quantity="1" data-price="53000000"><div class="itemName cashhack credits" data-cash="Avalanche MG14">Avalanche MG14</div> <span style="color: #c0c0c0;">(AC)</span><div class="tradeZone">Outpost</div><div class="seller">ScarHK</div><div class="salePrice" style="color: red;">$53,000,000</div><button disabled="" data-action="buyItem" data-item-location="1" data-buynum="350533865">buy</button></div> const row = document.createElement("div"); row.classList.add("fakeItem"); if (isPending) { row.classList.add("pending"); } row.setAttribute("data-type", entry.item || 'broken'); row.setAttribute("data-quantity", entry.quantity || 1); row.setAttribute("data-price", entry.price || 0); row.setAttribute("data-trade-id", entry.trade_id); row.addEventListener('click', function () { if (pageLock) return; const type = this.getAttribute('data-type'); if (type == 'broken') { alert('Entry is broken, contact Runon with this data: ' + JSON.stringify(entry)); if (confirm('Remove this entry instead?')) { // const tradeId = this.getAttribute('data-trade-id'); HISTORY.removeTrade(entryIndex, true); loadMarket(); } return; } const tradeId = this.getAttribute('data-trade-id'); const trade = HISTORY.cache.trade_id[tradeId]; if (!trade) { alert('Could not find trade in cache (ID: ' + tradeId + ')'); return; } HISTORY.renderEntryPrompt(trade); }); let afterName = entry.item ? calcMCTag(entry.item, false, "span", "") || '' : ''; const itemId = entry.item ? getGlobalDataItemId(entry.item) : 'broken'; const itemCat = entry.item ? getItemType(unsafeWindow.globalData[itemId]) : null; if (itemCat == 'ammo') { afterName += ' <span>(' + entry.quantity + ')</span>'; } const displayPrice = formatMoney(entry.price || 0, {showFree: true}); row.innerHTML = ` <div class="itemName cashhack credits" data-cash="${entry.itemname}">${entry.itemname}</div> ${afterName} <div class="tradeType">${entry.action}</div> <div class="salePrice">${displayPrice}</div> <div class="saleDate">${formatDate(new Date(entry.date))}</div> `; // row.innerHTML = "<div class='itemName cashhack credits' data-cash='" + entry.itemname + "'>" + entry.itemname + "</div><div class='tradeZone'>" + entry.trade_zone + "</div><div class='seller'>" + entry.member_name + "</div><div class='salePrice' style='color: red;'>$" + entry.price + "</div>"; historyItemDisplay.appendChild(row); } return true; }; const onHistoryScroll = function () { const fullScrollHeight = historyItemDisplay.scrollHeight; const scrolledHeight = historyItemDisplay.scrollTop + historyItemDisplay.clientHeight; const diff = fullScrollHeight - scrolledHeight; if (diff > 50) { return; } const perPage = parseInt(historyItemDisplay.getAttribute('data-per-page')); historyItemDisplay.removeEventListener('scroll', onHistoryScroll); historyItemDisplay.setAttribute('data-offset', parseInt(historyItemDisplay.getAttribute('data-offset')) + perPage); const hasMore = renderHistoryItems(); if (hasMore) { historyItemDisplay.addEventListener('scroll', onHistoryScroll); } }; marketHolder.appendChild(historyItemDisplay); marketHolder.appendChild(boxLabels); marketHolder.appendChild(historyResultsText); await HISTORY.load(); // retrieve current user's pending trades await new Promise(resolve => { const now = Date.now(); const lastCheck = WEBCALL_HOOKS.lastExecutedAt.trade_search || 0; // Check if last check was less than 30 seconds ago if (lastCheck && (now - lastCheck) < 30000) { resolve(); return; } var dataArray = {}; dataArray["pagetime"] = userVars["pagetime"]; dataArray["tradezone"] = ""; dataArray["searchname"] = ""; dataArray["searchtype"] = "sellinglist"; dataArray["search"] = "trades"; dataArray["memID"] = userVars["userID"]; dataArray["category"] = ""; dataArray["profession"] = ""; WEBCALL_HOOKS.lastExecutedAt.trade_search = now; // Execute webCall webCall("trade_search", dataArray, resolve, true); // Cache will be updated by webCall hook somewhere else in the code }); renderHistoryItems(); historyItemDisplay.addEventListener('scroll', onHistoryScroll); break; case 'stats': statsBtn.disabled = true; // const filterBox = document.createElement("div"); // filterBox.id = "historyFilterArea"; // Filter box shows items that are added to search on // But also a date range select // filterBox.innerHTML = // categorySelect += "<div style='display: inline-block; width: 260px;'>In Category:<br/><div id='categoryChoice' data-catname=''><span id='cat'>Everything</span><span id='dog' style='float: right;'>◄</span></div>"; // <div class="historyDetailsText">Click on a trade to see more info</div> const historyInfoBox = document.createElement("div"); historyInfoBox.id = "historyInfoBox"; if (HISTORY.selectedItem) { const globalStatisticItem = unsafeWindow.globalData[getGlobalDataItemId(HISTORY.selectedItem)]; const isAmmo = globalStatisticItem.itemcat == 'ammo'; const stackSize = maxStack(HISTORY.selectedItem); const perNamer = function (amount) { let perName = 'item'; if (isAmmo) { perName = 'round'; } if (HISTORY.selectedItem == 'fuelammo') { return 'mL'; } return perName + (amount == 1 ? '' : 's'); }; const perStackNamer = function (amount) { return 'stack' + (amount == 1 ? '' : 's'); }; // === START OF STATS RENDER // Title with timeframe const timeframe = SETTINGS.values.hoverStatisticsTimeframe; // Bought stats const amountBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'amount_bought'); const amountBoughtStacks = new Number(amountBought / stackSize).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 2}); const totalPriceBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'total_price_bought'); const avgPriceBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'avg_price_bought'); // Sold stats const amountSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'amount_sold'); const amountSoldStacks = new Number(amountSold / stackSize).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 2}); const totalPriceSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'total_price_sold'); const avgPriceSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'avg_price_sold'); // Profit/Loss stats const averageProfit = avgPriceSold - avgPriceBought; const totalProfit = totalPriceSold - totalPriceBought; const totalProfitItemCount = Math.min(amountSold, amountBought); let totalRealProfit = 0; if (totalProfitItemCount > 0) { totalRealProfit = (totalProfitItemCount * avgPriceSold) - (totalProfitItemCount * avgPriceBought); } const lastBoughtAt = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_date_bought'); const lastBoughtFor = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_price_bought'); const lastSoldAt = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_date_sold'); const lastSoldFor = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_price_sold'); // === END OF STATS RENDER historyInfoBox.innerHTML = ` <table> <tr class="row"> <td>Amount bought</td> <td> <span style="color: #FFCC00;">${amountBought}</span> ${perNamer(amountBought)} ${isAmmo ? `<br>≈ <span style="color: #FFCC00;">${amountBoughtStacks}</span> ${perStackNamer(amountBoughtStacks)}` : `` } </td> <td> for ${formatMoneyHtml(totalPriceBought, true)} total </td> </tr> <tr class="row"> <td>Amount sold</td> <td> <span style="color: #FFCC00;">${amountSold}</span> ${perNamer(amountSold)} ${isAmmo ? `<br>≈ <span style="color: #FFCC00;">${amountSoldStacks}</span> ${perStackNamer(amountSoldStacks)}` : `` } </td> <td> for ${formatMoneyHtml(totalPriceSold, true)} total </td> </tr> <tr class="row"> <td>Average buy price</td> <td> ${formatMoneyHtml(avgPriceBought, true)} per ${perNamer(1)} ${isAmmo ? `<br>${formatMoneyHtml(avgPriceBought * stackSize, true)} per ${perStackNamer(1)}` : `` } </td> <td> </td> </tr> <tr class="row"> <td>Average sell price</td> <td> ${formatMoneyHtml(avgPriceSold, true)} per ${perNamer(1)} ${isAmmo ? `<br>${formatMoneyHtml(avgPriceSold * stackSize, true)} per ${perStackNamer(1)}` : `` } </td> <td> </td> </tr> <tr class="row"> <td>Average profit/loss</td> <td> ${formatMoneyHtml(averageProfit, {neutralColor: false, maximumFractionDigits: 4})} per ${perNamer(1)} ${isAmmo ? `<br>${formatMoneyHtml(averageProfit * stackSize, {neutralColor: false, maximumFractionDigits: 4})} per ${perStackNamer(1)}` : `` } </td> <td> ${SETTINGS.values.countScraps ? '(With scraps)' : '(Without scraps)' } </td> </tr> <tr class="row"> <td>Real total profit/loss</td> <td> ${formatMoneyHtml(totalRealProfit, false)} </td> <td> (Based on <span style="color: #FFCC00;">${totalProfitItemCount}</span> buys & sells) </td> </tr> <tr class="row"> <td>Total profit/loss</td> <td> ${formatMoneyHtml(totalProfit, false)} </td> <td> (Based on <span style="color: #FFCC00;">${amountBought}</span> buys, <span style="color: #FFCC00;">${amountSold}</span> sells) </td> </tr> <tr class="row"> <td>Last bought</td> <td> ${lastBoughtAt ? `at <span style="color: #FFCC00;">${formatDate(new Date(lastBoughtAt))}</span>` : `Never bought` } </td> <td> ${lastBoughtFor ? `for ${formatMoneyHtml(lastBoughtFor, true)}` : `` } </td> </tr> <tr class="row"> <td>Last sold</td> <td> ${lastSoldAt ? `at <span style="color: #FFCC00;">${formatDate(new Date(lastSoldAt))}</span>` : `Never sold` } </td> <td> ${lastSoldFor ? `for ${formatMoneyHtml(lastSoldFor, true)}` : `` } </td> </tr> </table> `; } else { historyInfoBox.innerHTML = ` <div class="historyDetailsContainer"> <div class="historyDetailsText">Search for an item to see its statistics</div> </div> `; } // marketHolder.appendChild(filterBox); marketHolder.appendChild(historyInfoBox); break; } promptEnd(); break; } })(); } function initHistorySelects() { const historySelectComponents = document.getElementsByClassName('historySelectComponent'); for(const historySelectComponent of historySelectComponents) { if (historySelectComponent.dataset.init) { continue; } const initValue = historySelectComponent.dataset.value; const choiceElem = historySelectComponent.getElementsByClassName('selectChoice')[0]; const [name, dog] = choiceElem.children; dog.textContent = '◄'; const listElem = historySelectComponent.getElementsByClassName('selectList')[0]; const options = listElem.children; listElem.style.display = 'none'; const selectOption = function (value, isInit = false) { const eventObject = { target: historySelectComponent, value: value, canceled: false, cause: isInit ? 'init' : 'change', }; if (historySelectComponent.oncustomselect) { historySelectComponent.oncustomselect(eventObject); } if (eventObject.canceled) { return; } historySelectComponent.dataset.value = value; const label = Array.from(options).find(option => option.dataset.value == value)?.textContent; name.textContent = label; }; const toggleSelect = function () { const display = listElem.style.display; const isHidden = display == 'none'; if (isHidden) { listElem.style.display = 'block'; dog.textContent = '▼'; } else { listElem.style.display = 'none'; dog.textContent = '◄'; } }; choiceElem.addEventListener('click', toggleSelect); for(const option of options) { option.addEventListener('click', function () { const value = this.dataset.value; selectOption(value); toggleSelect(); }); } if (initValue) { selectOption(initValue, true); } historySelectComponent.dataset.init = true; } } /****************************************************** * Styles ******************************************************/ // GM_addStyle_object('#marketplace', {}); GM_addStyle_object('.cashhack.cashhack-relative', { '$ &:before': { position: 'relative', }, position: 'relative', }); GM_addStyle_object('.historyInfoContainer', { textAlign: 'left', }); GM_addStyle_object('#marketplace #historySettings', { position: 'absolute', top: '5px', right: '5px', width: '20px', height: '20px', backgroundImage: 'url(../images/df_gear.png)', backgroundSize: 'cover', cursor: 'pointer', }); GM_addStyle_object('#marketplace #historyItemDisplay', { top: '155px', bottom: '110px', }); GM_addStyle_object('.historyEntryForm', { '$ & input::placeholder': { color: 'rgba(255, 255, 0, 0.3)', }, }); GM_addStyle_object('#marketplace #historySearchArea', { position: 'absolute', top: '100px', left: '20px', // right: '80px', height: '16px', width: '250px', padding: '8px', border: '1px #990000 solid', textAlign: 'left', backgroundColor: 'rgba(0,0,0,0.8)', '$ & #historySearchField::placeholder': { color: 'rgba(255, 255, 0, 0.3)', }, '$ & #historyItemSearchResultBox': { position: 'absolute', // top: '184px', width: '250px', maxHeight: '300px', overflowY: 'auto', padding: '4px', border: '1px #990000 solid', textAlign: 'left', backgroundColor: 'rgba(0,0,0,0.8)', zIndex: '100', '$ &.hidden': { display: 'none', }, }, }); GM_addStyle_object('#marketplace #historyFilterArea', { position: 'absolute', top: '100px', left: '290px', right: '20px', height: '16px', padding: '8px', border: '1px #990000 solid', textAlign: 'left', backgroundColor: 'rgba(0,0,0,0.8)', '$ & #filterMinDate': { left: '110px', }, '$ & #filterMaxDate': { left: '210px', }, '$ & #filterGo': { left: '310px', }, }); GM_addStyle_object('#marketplace #historyInfoBox', { position: 'absolute', overflowY: 'auto', top: '141px', left: '20px', right: '20px', bottom: '110px', // padding: '8px', border: '1px #990000 solid', textAlign: 'left', backgroundColor: 'rgba(0,0,0,0.8)', '$ & .historyDetailsContainer': { fontSize: '14px', width: '100%', height: '100%', '$ & .historyDetailsText': { margin: '0', position: 'absolute', top: '50%', left: '50%', width: '100%', textAlign: 'center', transform: 'translateY(-50%) translateX(-50%)', } }, '$ & table': { fontSize: '12px', fontFamily: '"Courier New", "Arial"', lineHeight: '1', width: '100%', borderCollapse: 'collapse', // Table is full width, but only the last td takes up as much space, the rest is just as wide as the content '$ & td': { width: '1%', whiteSpace: 'nowrap', // padding: '4px', //padding x is 4px, padding y is 2px padding: '0px 4px', // border: '1px #990000 solid', textAlign: 'left', height: '32px', '$ &:first-child': { width: '200px', }, '$ &:last-child': { width: '100%', }, }, '$ & tr': { borderBottom: '1px #990000 solid', '$ &.row:hover': { backgroundColor: 'rgba(125, 0, 0, 0.4)', }, }, }, }); GM_addStyle_object('#marketplace #historyItemDisplay .fakeItem', { paddingLeft: '6px', fontSize: '9pt', height: '16px', cursor: 'pointer', userSelect: 'none', '$ &.pending': { opacity: '0.5', }, '$ &:hover': { backgroundColor: 'rgba(125, 0, 0, 0.8)', }, '$ & > div': { display: 'inline-block', position: 'relative', }, '$ & .tradeType': { position: 'absolute', left: '214px', color: '#00FF00', }, '$ & .salePrice': { position: 'absolute', left: '326px', color: '#FFCC00', }, '$ & .saleDate': { position: 'absolute', left: '486px', }, }); GM_addStyle_object('#marketplace #selectHistoryCategory', { position: 'absolute', width: '100%', top: '70px', fontSize: '12pt', '$ & button': { width: '120px', }, }); GM_addStyle_object('.historySelectComponent', { position: 'relative', '$ & .selectChoice': { position: 'absolute', cursor: 'pointer', width: '80px', display: 'inline-block', textAlign: 'center', backgroundColor: '#222', border: '1px solid #990000', '$ & span:last-child': { position: 'absolute', right: '0', }, }, '$ & .selectList': { position: 'absolute', display: 'none', position: 'absolute', zIndex: '10', top: '18px', width: '80px', // overflowY: 'auto', // left: '193px', backgroundColor: '#111', // borderLeft: '1px solid #990000', border: '1px solid #990000', textAlign: 'center', '$ & div': { cursor: 'pointer', '$ &:hover': { backgroundColor: '#333', }, }, }, }); GM_addStyle_object('#selectedItemsWrapper', { width: '200px', top: '8px', left: '40px', '$ & .historySelectComponent': { width: '100%', '$ & .selectChoice': { width: '100%', }, '$ & .selectList': { width: '100%', }, }, }); GM_addStyle_object('#filterActionTypeWrapper', { width: '100px', // top: '8px', // left: '40px', '$ & .historySelectComponent': { width: '100%', '$ & .selectChoice': { width: '100%', }, '$ & .selectList': { width: '100%', }, }, }); /****************************************************** * DF Function Overrides ******************************************************/ // Source: market.js // Explanation: // Allows this script to add a 'history' tab seemlessly into the marketplace // This approach should make it still compatible with other userscripts and official site scripts. const origLoadMarket = unsafeWindow.loadMarket; unsafeWindow.loadMarket = function() { console.log('override loadmarket'); // Execute original function origLoadMarket.apply(unsafeWindow, arguments); injectHistoryTabIntoMarketplace(); }; // Source: base.js // Explanation: // Allows this script to hook into before and after the callback of webCall. // Which prevents us having to do extra requests while still getting the data we need // The less requests, the better. // Plus DeadFrontier's webCalls are executed at exactly the right moments we need (like after selling) // This approach should make it still compatible with other userscripts and official site scripts. const originalWebCall = unsafeWindow.webCall; unsafeWindow.webCall = function (call, params, callback, hashed) { // Override the callback function to execute any hooks // This still executes the original callback function, but with our hooks const callbackWithHooks = function(data, status, xhr) { const dataObj = stringExplode(data) const response = stringExplode(xhr.responseText); // Call all 'before' hooks if (WEBCALL_HOOKS.before.hasOwnProperty(call)) { // Copy the array, incase that hooks remove themselves during their execution const beforeHooks = WEBCALL_HOOKS.before[call].slice(); for (const beforeHook of beforeHooks) { beforeHook( { call, params, callback, hashed, }, { dataObj, response, data, status, xhr, } ); } } // Call all 'beforeAll' hooks const beforeAllHooks = WEBCALL_HOOKS.beforeAll.slice(); for (const beforeAllHook of beforeAllHooks) { beforeAllHook( { call, params, callback, hashed, }, { dataObj, response, data, status, xhr, } ); } // Execute the original callback const result = callback.call(unsafeWindow, data, status, xhr); // Call all 'after' hooks if (WEBCALL_HOOKS.after.hasOwnProperty(call)) { // Copy the array, incase that hooks remove themselves during their execution const afterHooks = WEBCALL_HOOKS.after[call].slice(); for (const afterHook of afterHooks) { afterHook( { call, params, callback, hashed, }, { dataObj, response, data, status, xhr, }, result ); } } // Call all 'afterAll' hooks const afterAllHooks = WEBCALL_HOOKS.afterAll.slice(); for (const afterAllHook of afterAllHooks) { afterAllHook( { call, params, callback, hashed, }, { dataObj, response, data, status, xhr, }, result ); } // Return the original callback result // As far as I see in the source code, the callbacks never return anything, but its cleaner to return it anyway return result; }; // Call the original webCall function, but with our hooked callback function return originalWebCall.call(unsafeWindow, call, params, callbackWithHooks, hashed); }; // Bugfix for DeadFrontier code const origAllowedInfoCard = unsafeWindow.allowedInfoCard; unsafeWindow.allowedInfoCard = function (elem) { if(elem && typeof elem.classList !== "undefined" && (elem.classList.contains("item") || elem.classList.contains("fakeItem") || elem.parentNode?.classList.contains("fakeItem"))) { return true; } else { return false; } } // Source: inventory.js // Explanation: // Allows this script to hook into the infoCard function, which is used to display item info when hovering over an item // This approach makes it still compatible with SilverScript's HoverPrices var origInfoCard = unsafeWindow.infoCard || null; if (origInfoCard) { inventoryHolder.removeEventListener("mousemove", origInfoCard, false); unsafeWindow.infoCard = function (e) { // infoBox.style.color = ''; //Remove previous history info let elems = document.getElementsByClassName("historyInfoContainer"); for(var i = elems.length - 1; i >= 0; i--) { elems[i].parentNode.removeChild(elems[i]); } elems = document.getElementsByClassName("historyShiftNotice"); for(var i = elems.length - 1; i >= 0; i--) { elems[i].parentNode.removeChild(elems[i]); } // Call the original infoCard function origInfoCard(e); if(active || pageLock || !allowedInfoCard(e.target)) { return; } var target; if(e.target.parentNode.classList.contains("fakeItem")) { target = e.target.parentNode; } else { target = e.target; } // if (!wasHidden) { // return; // } // Used in the history tab if (target.classList.contains('pending')) { const container = document.createElement('div'); // container.className = 'itemData historyInfoContainer'; container.classList.add('itemData'); container.classList.add('historyInfoContainer'); container.style.color = '#FFCC00'; container.style.marginTop = 'auto'; container.innerHTML = 'This sale is still pending'; infoBox.appendChild(container); } if (target.classList.contains('item') && SETTINGS.values.hoverEnabled) { HOVER_INFOBOX_DATA.event = e; if (SETTINGS.values.shiftHoverMode !== 'disabled') { const shiftHoverStyle = document.createElement('style'); shiftHoverStyle.classList.add('historyInfoContainer'); // Will be removed when infoCard is called again if (SETTINGS.values.shiftHoverMode == 'history') { const classNameToHide = e.shiftKey ? 'silverStats' : 'historyData'; shiftHoverStyle.innerHTML = '.' + classNameToHide + ' { display: none; }'; } if (SETTINGS.values.shiftHoverMode == 'silverscripts') { const classNameToHide = e.shiftKey ? 'historyData' : 'silverStats'; shiftHoverStyle.innerHTML = '.' + classNameToHide + ' { display: none; }'; } infoBox.appendChild(shiftHoverStyle); } const infoContainer = document.createElement('div'); const isAmmo = target.dataset.itemtype == 'ammo'; const item = target.dataset.type; const quantity = parseInt(target.dataset.quantity); infoContainer.classList.add('historyInfoContainer'); infoContainer.classList.add('itemData'); infoContainer.classList.add('historyData'); let infoText = ''; const perNamer = function (amount) { let perName = 'unit'; if (isAmmo) { perName = 'round'; } if (item == 'fuelammo') { return 'mL'; } return perName + (amount == 1 ? '' : 's'); }; if (SETTINGS.values.hoverAvgBuyPriceEnabled) { const avgPriceBought = HISTORY.getItemInfo(target.dataset.type, 'avg_price_bought'); infoText += 'Average buy price: ' + formatMoneyHtml(avgPriceBought, true) + '/' + perNamer(1); if (isAmmo) { infoText += ', ' + formatMoneyHtml(avgPriceBought * quantity, true) + '/stack(' + quantity + ')'; } infoText += '<br>'; } if (SETTINGS.values.hoverAvgSellPriceEnabled) { const avgPriceSold = HISTORY.getItemInfo(target.dataset.type, 'avg_price_sold'); infoText += 'Average sell price: ' + formatMoneyHtml(avgPriceSold, true) + '/' + perNamer(1); if (isAmmo) { infoText += ', ' + formatMoneyHtml(avgPriceSold * quantity, true) + '/stack(' + quantity + ')'; } infoText += '<br>'; } if (SETTINGS.values.hoverAmountBoughtEnabled) { const amountBought = HISTORY.getItemInfo(target.dataset.type, 'amount_bought'); infoText += 'Amount bought: ' + amountBought + '<br>'; } if (SETTINGS.values.hoverAmountSoldEnabled) { const amountSold = HISTORY.getItemInfo(target.dataset.type, 'amount_sold'); infoText += 'Amount sold: ' + amountSold + '<br>'; } if (SETTINGS.values.hoverLastBuyPriceEnabled) { const lastBuyPrice = HISTORY.getItemInfo(target.dataset.type, 'last_price_bought'); if (isAmmo) { const lastBuyQuantity = HISTORY.getItemInfo(target.dataset.type, 'last_quantity_bought'); const lastBuyPerRound = lastBuyPrice === null ? null : (lastBuyPrice / lastBuyQuantity); const lastBuyPerStack = lastBuyPrice === null ? null : (lastBuyPerRound * quantity); infoText += 'Last bought for: ' + (lastBuyPerRound === null ? 'Never bought' : formatMoneyHtml(lastBuyPerRound, true) + '/round, ' + formatMoneyHtml(lastBuyPerStack, true) + '/stack(' + quantity + ')') + '<br>'; } else { infoText += 'Last bought for: ' + (lastBuyPrice === null ? 'Never bought' : formatMoneyHtml(lastBuyPrice, true)) + '<br>'; } } if (SETTINGS.values.hoverLastSellPriceEnabled) { const lastSellPrice = HISTORY.getItemInfo(target.dataset.type, 'last_price_sold'); if (isAmmo) { const lastSellQuantity = HISTORY.getItemInfo(target.dataset.type, 'last_quantity_sold'); const lastSellPerRound = lastSellPrice === null ? null : (lastSellPrice / lastSellQuantity); const lastSellPerStack = lastSellPrice === null ? null : (lastSellPerRound * quantity); infoText += 'Last sold for: ' + (lastSellPerRound === null ? 'Never sold' : formatMoneyHtml(lastSellPerRound, true) + '/round, ' + formatMoneyHtml(lastSellPerStack, true) + '/stack(' + quantity + ')') + '<br>'; } else { infoText += 'Last sold for: ' + (lastSellPrice === null ? 'Never sold' : formatMoneyHtml(lastSellPrice, true)) + '<br>'; } } if (SETTINGS.values.hoverAvgProfitEnabled) { const avgPriceSold = HISTORY.getItemInfo(target.dataset.type, 'avg_price_sold'); const avgPriceBought = HISTORY.getItemInfo(target.dataset.type, 'avg_price_bought'); const avgProfit = avgPriceSold - avgPriceBought; infoText += 'Average profit/loss: ' + formatMoneyHtml(avgProfit, false) + '/' + perNamer(1); if (isAmmo) { const avgProfitStack = avgProfit * quantity; infoText += ', ' + formatMoneyHtml(avgProfitStack, false) + '/stack(' + quantity + ')'; } infoText += '<br>'; } if (infoText.trim()) { infoText = '<div style="text-decoration: underline; text-align: center;">History Data</div>' + infoText; } if (SETTINGS.values.hoverEnabled && SETTINGS.values.shiftHoverMode == 'silverscripts' && !e.shiftKey && silverScriptsInstalled) { infoText += '<div style="text-decoration: underline; font-size: 8pt;">Hold SHIFT to show SilverScript\'s HoverPrices</div>' } if (SETTINGS.values.hoverEnabled && SETTINGS.values.shiftHoverMode == 'history' && !e.shiftKey) { const historyShiftNotice = document.createElement('div'); historyShiftNotice.classList.add('historyShiftNotice'); historyShiftNotice.innerHTML = '<div style="text-decoration: underline; font-size: 8pt;">Hold SHIFT to show History Data</div>' infoBox.appendChild(historyShiftNotice); } infoContainer.innerHTML = infoText; infoBox.appendChild(infoContainer); } }.bind(unsafeWindow); inventoryHolder.addEventListener("mousemove", unsafeWindow.infoCard, false); } // Source: market.js var origSellMenuItemPopulate = unsafeWindow.SellMenuItemPopulate; unsafeWindow.SellMenuItemPopulate = function (itemElem) { // Call original function origSellMenuItemPopulate(itemElem); }; /****************************************************** * Webcall hooks ******************************************************/ // Hook into when an item is sold onBeforeWebCall('inventory_new', function (request, response) { if (request.params.action !== 'newsell') { return; } if (response.xhr.status != 200) { return; } // if (!response.dataObj.hasOwnProperty('OK') && response.dataObj.done != '1') { // return; // } // When the sell is successful, DeadFrontier will do a new webCall to retrieve the new sell listing // We hook ONCE into this webCall, to retrieve the trade id const onSellSuccess = function (request, response) { if (response.xhr.status == 200) { HISTORY.onSellItem(request, response); } // Remove self from hook offAfterWebCall('trade_search', onSellSuccess); }; // Hook into the new sell listing webCall onAfterWebCall('trade_search', onSellSuccess); }); // Hook into when credits are sold onBeforeWebCall('inventory_new', function (request, response) { if (request.params.action !== 'newsellcredits') { return; } if (response.xhr.status != 200) { return; } // if (!response.dataObj.hasOwnProperty('OK') && response.dataObj.done != '1') { // return; // } // When the sell is successful, DeadFrontier will do a new webCall to retrieve the new sell listing // We hook ONCE into this webCall, to retrieve the trade id const onSellSuccess = function (request, response) { if (response.xhr.status == 200) { HISTORY.onSellItem(request, response); } // Remove self from hook offAfterWebCall('trade_search', onSellSuccess); }; // Hook into the new sell listing webCall onAfterWebCall('trade_search', onSellSuccess); }); // Hook into when an item is bought onAfterWebCall('inventory_new', function (request, response) { if (request.params.action !== 'newbuy') { return; } if (response.xhr.status != 200) { return; } if (!response.dataObj.hasOwnProperty('OK')) { return; } const dataObj = {}; for(const key in response.dataObj) { if (key.indexOf('df_inv') !== 0) { continue; } dataObj[key.replace(/^df_inv\d+_/, '')] = response.dataObj[key]; } const entry = { trade_id: request.params.buynum, action: 'buy', price: request.params.expected_itemprice, item: dataObj.type, itemname: unsafeWindow.itemNamer(dataObj.type, dataObj.quantity), quantity: dataObj.quantity, }; HISTORY.pushTrade(entry); }); // Hook into when an item is scrapped onBeforeWebCall('inventory_new', function (request, response) { if (request.params.action !== 'scrap') { return; } if (response.xhr.status != 200) { return; } if (!response.dataObj.hasOwnProperty('OK')) { return; } const itemnum = request.params.itemnum; const quantity = unsafeWindow.userVars['DFSTATS_df_inv' + itemnum + '_quantity']; const itemTypeId = unsafeWindow.userVars['DFSTATS_df_inv' + itemnum + '_type']; if (!itemTypeId) { const logData = { price: request.params.price, item: request.params.expected_itemtype, itemnum, itemTypeId, quantity, }; alert('Error: Could not find item type id for scrapped item\n\nContact Runonstof with this data: ' + JSON.stringify(logData)); alert('You can also check the console (F12) for the data to share with Runonstof'); console.info(JSON.stringify(logData, null, 2)); console.info('Only share above data with Runonstof'); return; } const entry = { trade_id: hash(objectJoin(request.params)), action: 'scrap', price: request.params.price, item: request.params.expected_itemtype, itemname: unsafeWindow.itemNamer(itemTypeId, quantity), quantity, }; HISTORY.pushTrade(entry); }); // Hook into when a sale is canceled onAfterWebCall('inventory_new', function (request, response) { if (request.params.action !== 'newcancelsale') { return; } if (response.xhr.status != 200) { return; } const tradeId = request.params.buynum; HISTORY.removeTrade(tradeId); }); // Update 'pending sales' trade cache onAfterWebCall('trade_search', function (request, response) { if (response.xhr.status != 200) { return; } const tradeCount = response.dataObj.tradelist_totalsales; if (tradeCount == 0) { return; } const pendingTradeIds = []; for(let i = 0; i < tradeCount; i++) { const tradeId = response.dataObj['tradelist_' + i + '_trade_id']; pendingTradeIds.push(tradeId); } HISTORY.cache.pending_trade_ids = pendingTradeIds; }); /****************************************************** * Await Page Initialization ******************************************************/ console.log('awaiting page initialization'); // A promise that resolves when document is fully loaded and globalData is filled with stackables // This is because DeadFrontier does a request to stackables.json, which is needed for the max stack of items // Only after this request is done, globalData will contain ammo with a max_quantity await new Promise(resolve => { if (unsafeWindow.globalData.hasOwnProperty('32ammo')) { resolve(); return; } // This is the original function that is called when the stackables.json request is done const origUpdateIntoArr = unsafeWindow.updateIntoArr; unsafeWindow.updateIntoArr = function (flshArr, baseArr) { // Execute original function origUpdateIntoArr.apply(unsafeWindow, [flshArr, baseArr]); // Check if globalData is filled with stackables if (unsafeWindow.globalData != baseArr) { return; } // revert override, we dont need it anymore unsafeWindow.updateIntoArr = origUpdateIntoArr; resolve(); } }); /****************************************************** * Script Initialization ******************************************************/ SEARCHABLE_ITEMS = Object.keys(unsafeWindow.globalData) .filter(itemId => !['brokenitem', 'undefined'].includes(itemId) && unsafeWindow.globalData[itemId].no_transfer != '1'); SEARCHABLE_ITEMS.forEach(itemId => { const item = unsafeWindow.globalData[itemId]; if (!item.needcook || item.needcook != '1') { return; } SEARCHABLE_ITEMS.push(itemId + '_cooked'); }); unsafeWindow.SEARCHABLE_ITEMS = SEARCHABLE_ITEMS; // Load History console.log('awaiting history initialization'); await HISTORY.init(); HISTORY.resetCache(); // HISTORY.initCache(); // Load settings console.log('awaiting settings initialization'); await SETTINGS.load(); //Populate LOOKUP for (const itemId in unsafeWindow.globalData) { const item = unsafeWindow.globalData[itemId]; const categoryId = item.itemcat; if (!LOOKUP.category__item_id.hasOwnProperty(categoryId)) { LOOKUP.category__item_id[categoryId] = []; } } for (const categoryId in LOOKUP.category__item_id) { LOOKUP.category__item_id[categoryId].sort((a, b) => { const itemA = unsafeWindow.globalData[a]; const itemB = unsafeWindow.globalData[b]; const nameA = itemA.name?.toLowerCase() || ''; const nameB = itemB.name?.toLowerCase() || ''; return nameA.localeCompare(nameB); }); } delete LOOKUP.category__item_id['broken']; // DEBUG unsafeWindow.LOOKUP = LOOKUP; unsafeWindow.SETTINGS = SETTINGS; unsafeWindow.HISTORY = HISTORY; var historySettingsButton = document.createElement("button"); historySettingsButton.classList.add("opElem"); historySettingsButton.style.left = page == 35 ? "200px" : "400px"; historySettingsButton.style.bottom = "86px"; historySettingsButton.textContent = "History Menu"; inventoryHolder.appendChild(historySettingsButton); historySettingsButton.addEventListener("click", function () { const fn = SETTINGS.renderSettingsPrompt.bind(SETTINGS); fn(); }); console.log('awaiting ready'); await ready(); document.getElementById("invController").removeEventListener("contextmenu", unsafeWindow.openSellContextMenu, false); document.getElementById("invController").addEventListener("contextmenu", unsafeWindow.openSellContextMenu, false); const onShiftRelease = function (event) { if (event.key != 'Shift') { return; } // console.log('shift hover mode: ' + SETTINGS.values.shiftHoverMode); HOVER_INFOBOX_DATA.run(false); unsafeWindow.document.removeEventListener('keyup', onShiftRelease); }; unsafeWindow.document.addEventListener('keydown', function (event) { if (event.key != 'Shift') { return; } // console.log('shift hover mode: ' + SETTINGS.values.shiftHoverMode); HOVER_INFOBOX_DATA.run(true); unsafeWindow.document.addEventListener('keyup', onShiftRelease); }); // Create button if script loaded too early if (page == 35) { injectHistoryTabIntoMarketplace(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址