您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows your storage, but as a list. Features filters and sorting.
// ==UserScript== // @name DF Storage List // @namespace http://tampermonkey.net/ // @version 1.1 // @description Shows your storage, but as a list. Features filters and sorting. // @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 storage page, stop script if (page != 50) { return; } /****************************************************** * Global variables ******************************************************/ const WEBCALL_HOOKS = { before: {}, after: {}, afterAll: [], beforeAll: [], }; const SORT_OPTIONS = { '': { label: 'none', strategy: null, }, 'name': { label: 'name', strategy: 'string', getter: (item) => unsafeWindow.itemNamer(item.type, item.quantity), }, 'quantity': { label: 'quantity', strategy: 'number', getter: (item) => parseInt(item.quantity), }, 'scrap': { label: 'scrap value', strategy: 'number', getter: (item) => unsafeWindow.scrapValue(item.type, item.quantity), }, }; const ITEM_TYPE_OPTIONS = { weapon: { label: 'Weapons', values: ['weapon'], }, ammo: { label: 'Ammo', values: ['ammo'], }, armour: { label: 'Armour', values: ['armour'], }, item: { label: 'Items', values: ['item'], }, implant: { label: 'Implants', values: ['implant'], }, clothing: { label: 'Clothing', values: ['hat', 'mask', 'coat', 'shirt', 'trousers'], }, }; const LOOKUP = { category__item_id: {}, item_types: [], }; unsafeWindow.LOOKUP = LOOKUP; /****************************************************** * Utility functions ******************************************************/ function GM_addStyle(css) { const style = document.getElementById("GM_addStyle_Runon_storage_list") || (function() { const style = document.createElement('style'); style.type = 'text/css'; style.id = "GM_addStyle_Runon_storage_list"; 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); } } // 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 getGlobalDataItemId(rawItemId) { return rawItemId.split('_')[0]; } function getBaseItemId(rawItemId) { return rawItemId.replace(/_stats\d+/, ''); } /** * 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); } /****************************************************** * Storage list object ******************************************************/ const STORAGE_LIST = { shown: false, maxItems: 480, initialized: false, values: { search: '', sort: '', sortDirection: 'asc', types: [], // If empty, all types are shown, contains 'item', 'ammo', 'armour', 'implant' scrollY: 0, }, async load() { // this.shown = true; this.shown = await GM.getValue('storage_list_shown', false); const values = await GM.getValue('storage_list_values', {}); mergeDeep(this.values, values); }, async save() { await GM.setValue('storage_list_values', this.values); }, saveDebounced() { if (this.saveDebounceTimeout) { clearTimeout(this.saveDebounceTimeout); } this.saveDebounceTimeout = setTimeout(() => { this.save(); }, 200); }, async toggle() { this.shown = !this.shown; await GM.setValue('storage_list_shown', this.shown); this.updateInventoryHolder(); }, storageKey(key) { return 'storage_list_' + key + '_' + userVars.userID; }, *iterator() { for(let i = 0; i < this.maxItems; i++) { const item = this.item(i); if (!item) { continue; } yield item; } }, count() { return Array.from(this.iterator()).length; }, items() { let items = Array.from(this.iterator()); if (this.values.types.length || this.values.search) { const allowedItemTypes = this.values.types.flatMap(itemType => ITEM_TYPE_OPTIONS[itemType].values); items = items.filter(item => { if (this.values.search) { const itemName = unsafeWindow.itemNamer(item.type, item.quantity).toLowerCase(); if (!itemName.includes(this.values.search.toLowerCase()) && !item.type.includes(this.values.search.toLowerCase())) { return false; } } if (!allowedItemTypes.length) { return true; } const itemId = getGlobalDataItemId(item.type); const itemType = getItemType(unsafeWindow.globalData[itemId]); return allowedItemTypes.includes(itemType); }); } if (this.values.sort) { items.sort((a, b) => { const strategy = SORT_OPTIONS[this.values.sort].strategy; if (!strategy) { return 0; } const getter = SORT_OPTIONS[this.values.sort].getter; const valueA = getter(a); const valueB = getter(b); if (strategy === 'string') { return valueA.localeCompare(valueB); } if (strategy === 'number') { return valueA - valueB; } return 0; }); if (this.values.sortDirection === 'desc') { items.reverse(); } } return items; }, item(index) { if (!unsafeWindow.storageBox.hasOwnProperty('df_store' + index + '_type')) { return null; } return { slot: index, type: unsafeWindow.storageBox['df_store' + index + '_type'], quantity: unsafeWindow.storageBox['df_store' + index + '_quantity'], }; }, updateInventoryHolder() { if (this.shown) { // Hide the regular storage box unsafeWindow.inventoryholder.classList.add('hide-box'); // Show the storage list unsafeWindow.storageListingHolder.classList.remove('hide-list'); } else { // Show the regular storage box unsafeWindow.inventoryholder.classList.remove('hide-box'); // Hide the storage list unsafeWindow.storageListingHolder.classList.add('hide-list'); } }, // removeStorageListHolder() { // const storageListElem = document.getElementById('storageListing'); // if (storageListElem) { // storageListElem.remove(); // } // }, init() { if (this.initialized) { return; } // Insert the storage list holder const storageListHolderElem = document.createElement('div'); storageListHolderElem.id = 'storageListingHolder'; unsafeWindow.inventoryholder.appendChild(storageListHolderElem); // Insert count display const countDisplay = document.createElement('div'); countDisplay.id = 'storageListCount'; countDisplay.style.position = 'absolute'; countDisplay.style.left = '150px'; countDisplay.style.top = '54px'; countDisplay.style.textAlign = 'left'; countDisplay.style.fontSize = '9pt'; countDisplay.style.width = '400px'; countDisplay.style.height = '20px'; unsafeWindow.inventoryholder.appendChild(countDisplay); // Insert search input let renderTimeout; const searchInput = document.createElement('input'); searchInput.id = 'storageListSearch'; searchInput.placeholder = 'Search'; searchInput.value = this.values.search; searchInput.addEventListener('input', function () { STORAGE_LIST.values.search = this.value; STORAGE_LIST.values.scrollY = 0; STORAGE_LIST.saveDebounced(); const render = function () { STORAGE_LIST.render(); }; if (renderTimeout) { clearTimeout(renderTimeout); } renderTimeout = setTimeout(render, 250); }); searchInput.style.position = 'absolute'; searchInput.style.left = '0'; searchInput.style.top = '90px'; searchInput.style.width = '130px'; unsafeWindow.inventoryholder.appendChild(searchInput); // Insert toggle button const toggleButton = document.createElement('button'); toggleButton.id = 'toggleStorageList'; toggleButton.textContent = this.shown ? 'Show box' : 'Show list'; toggleButton.addEventListener('click', async function () { await STORAGE_LIST.toggle(); if (STORAGE_LIST.shown) { this.textContent = 'Show box'; } else { this.textContent = 'Show list'; } }); toggleButton.style.position = 'absolute'; toggleButton.style.right = '160px'; toggleButton.style.top = '70px'; unsafeWindow.inventoryholder.appendChild(toggleButton); // Insert toggle sort button const toggleSortButton = document.createElement('button'); toggleSortButton.id = 'toggleStorageListSort'; const getSortLabel = function () { const sortLabel = SORT_OPTIONS[STORAGE_LIST.values.sort].label; if (STORAGE_LIST.values.sort === '') { return `Sort by:<br>none`; } const sortDirectionLabel = STORAGE_LIST.values.sortDirection === 'asc' ? 'asc' : 'desc'; return `Sort by:<br>${sortLabel} (${sortDirectionLabel})`; }; toggleSortButton.innerHTML = getSortLabel(); toggleSortButton.addEventListener('click', async function () { // if empty then goto next sort option immediately if (STORAGE_LIST.values.sort === '') { STORAGE_LIST.values.sort = 'name'; STORAGE_LIST.values.sortDirection = 'asc'; } else { // if asc, goto desc, if desc, goto next sort option if (STORAGE_LIST.values.sortDirection === 'asc') { STORAGE_LIST.values.sortDirection = 'desc'; }else { const sortKeys = Object.keys(SORT_OPTIONS); const currentSortIndex = sortKeys.indexOf(STORAGE_LIST.values.sort); const nextSortIndex = currentSortIndex + 1; if (nextSortIndex >= sortKeys.length) { STORAGE_LIST.values.sort = ''; } else { STORAGE_LIST.values.sort = sortKeys[nextSortIndex]; } STORAGE_LIST.values.sortDirection = 'asc'; } } STORAGE_LIST.values.scrollY = 0; this.innerHTML = getSortLabel(); await STORAGE_LIST.save(); STORAGE_LIST.render(); }); toggleSortButton.style.position = 'absolute'; toggleSortButton.style.left = '0'; toggleSortButton.style.top = '120px'; toggleSortButton.style.textAlign = 'left'; unsafeWindow.inventoryholder.appendChild(toggleSortButton); // Insert filter buttons let optionIndex = 0; for(const itemType in ITEM_TYPE_OPTIONS) { const itemTypeOption = ITEM_TYPE_OPTIONS[itemType]; const getOptionLabel = function () { const isChecked = STORAGE_LIST.values.types.includes(itemType); return `[${isChecked ? 'x' : ' '}] ${itemTypeOption.label}`; }; const toggleOptionButton = document.createElement('button'); toggleOptionButton.classList.add('toggleStorageListOption'); toggleOptionButton.id = 'toggleStorageListOption_' + itemType; toggleOptionButton.innerHTML = getOptionLabel(); toggleOptionButton.addEventListener('click', async function () { const isChecked = STORAGE_LIST.values.types.includes(itemType); if (isChecked) { STORAGE_LIST.values.types.splice(STORAGE_LIST.values.types.indexOf(itemType), 1); } else { STORAGE_LIST.values.types.push(itemType); } STORAGE_LIST.values.scrollY = 0; this.innerHTML = getOptionLabel(); await STORAGE_LIST.save(); STORAGE_LIST.render(); }); toggleOptionButton.style.position = 'absolute'; toggleOptionButton.style.left = '0'; toggleOptionButton.style.top = (180 + (optionIndex * 14)) + 'px'; toggleOptionButton.style.textAlign = 'left'; unsafeWindow.inventoryholder.appendChild(toggleOptionButton); optionIndex++; } this.updateInventoryHolder(); this.initialized = true; }, render() { unsafeWindow.storageListingHolder.innerHTML = ''; const storageListElement = document.createElement('div'); storageListElement.id = 'storageListing'; storageListElement.addEventListener('scroll', function () { STORAGE_LIST.values.scrollY = this.scrollTop; STORAGE_LIST.saveDebounced(); }); unsafeWindow.storageListingHolder.appendChild(storageListElement); const items = this.items(); const userSlots = unsafeWindow.userVars.DFSTATS_df_storage_slots; const isFiltered = items.length < this.count(); unsafeWindow.storageListCount.innerHTML = `${isFiltered ? '' : '<br>'}Space used ${this.count()}/${userSlots}`; if (isFiltered) { unsafeWindow.storageListCount.innerHTML += `<br>Showing ${items.length} results`; } const hasInvSpace = unsafeWindow.findFirstEmptyGenericSlot('inv') !== false; for(const item of items) { const itemElem = document.createElement('div'); itemElem.classList.add('fakeItem'); itemElem.classList.add('listItem'); itemElem.dataset.type = item.type; itemElem.dataset.quantity = item.quantity; const itemId = getGlobalDataItemId(item.type); itemElem.dataset.itemtype = unsafeWindow.getItemType(unsafeWindow.globalData[itemId]); const itemName = unsafeWindow.itemNamer(item.type, item.quantity); const quantityText = item.quantity > 1 ? `(${item.quantity})` : ''; itemElem.innerHTML = ` <div class="itemName cashhack credits" data-cash="${itemName}">${itemName}</div> ${quantityText} `; const takeButton = document.createElement('button'); takeButton.classList.add('takeButton'); takeButton.classList.add('opElem'); if (!hasInvSpace) { takeButton.disabled = true; } else { takeButton.addEventListener('click', function () { if (this.disabled) { return; } const invSlot = unsafeWindow.findFirstEmptyGenericSlot("inv"); if (invSlot === false) { return; } const itemData = [ item.slot, item.type, 'storage', ]; const extraData = [itemData]; extraData[1] = [invSlot, "", "inventory"]; this.disabled = true; unsafeWindow.updateInventory(extraData); }); } takeButton.textContent = 'Take'; takeButton.style.right = '10px'; itemElem.appendChild(takeButton); storageListElement.appendChild(itemElem); } storageListElement.scrollTop = this.values.scrollY; }, }; unsafeWindow.STORAGE_LIST = STORAGE_LIST; /****************************************************** * Styles ******************************************************/ GM_addStyle_object('#inventoryholder', { '$ &.hide-box': { '$ & #storage': { display: 'none', }, '$ & #storageBackward, & #storageForward': { display: 'none!important', }, '$ & #buyStorageSlots': { display: 'none', '$ & + .opElem': { display: 'none', }, }, }, '$ &:not(.hide-box)': { '$ & #storageListSearch, & #toggleStorageListSort, & .toggleStorageListOption, & #storageListCount': { display: 'none', }, } }); GM_addStyle_object('#storageListingHolder', { '$ &.hide-list': { display: 'none', }, '$ & #storageListing': { position: 'relative', overflowY: 'auto', marginLeft: 'auto', marginRight: 'auto', border: '1px solid #990000', backgroundColor: 'rgba(0,0,0,0.8)', top: '91px', width: '400px', height: '320px', '$ & .listItem': { position: 'relative', width: 'calc(100% - 6px)', paddingLeft: '6px', textAlign: 'left', fontSize: '9pt', height: '16px', '$ & > div': { display: 'inline-block', position: 'relative', }, '$ & + .listItem': { borderTop: '1px #330000 solid', }, '$ & .itemName': { // paddingLeft: '6px', // '$ &.cashhack:before, &.cashhack:after': { // position: 'absolute', // }, margin: 'auto 0', }, '$ &:hover': { backgroundColor: 'rgba(125, 0, 0, 0.4)', }, }, }, }); // GM_addStyle_object('#selectCategory', { // position: 'absolute', // width: '100%', // top: '40px', // fontSize: '12pt', // }); /****************************************************** * DF Function Overrides ******************************************************/ // 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 = Object.fromEntries(new URLSearchParams(data).entries()); const responseDataObj = Object.fromEntries(new URLSearchParams(xhr.responseText).entries()); const request = { call, params, callback, hashed, }; const response = { dataObj, response: responseDataObj, data, status, xhr, }; // 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(request, response); } } // Call all 'beforeAll' hooks const beforeAllHooks = WEBCALL_HOOKS.beforeAll.slice(); for (const beforeAllHook of beforeAllHooks) { beforeAllHook(request, response); } // 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(request, response, result); } } // Call all 'afterAll' hooks const afterAllHooks = WEBCALL_HOOKS.afterAll.slice(); for (const afterAllHook of afterAllHooks) { afterAllHook(request, response, 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); }; /****************************************************** * Await 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 console.log('awaitin page init'); 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(); } }); console.log('awaitin storage init'); await new Promise(resolve => { if (unsafeWindow.storageBox) { resolve(); return; } let checkExistsInterval; const checkExists = () => { if (unsafeWindow.storageBox) { clearInterval(checkExistsInterval); resolve(); } }; checkExistsInterval = setInterval(checkExists, 100); }); // Wait until #normalContainer exists, resolve if it already exists console.log('awaitin normalContainer'); await new Promise(resolve => { const checkExists = () => { console.log('checking'); if (document.getElementById('normalContainer')) { resolve(); return true; } return false; }; const exists = checkExists(); if (exists) { return; } const checkExistInterval = setInterval(function() { if (checkExists()) { clearInterval(checkExistInterval); } }, 100); }); //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] = []; } LOOKUP.category__item_id[categoryId].push(itemId); const itemType = getItemType(item); if (!LOOKUP.item_types.includes(itemType)) { LOOKUP.item_types.push(itemType); } } 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']; /****************************************************** * Script start ******************************************************/ console.log('loading'); await STORAGE_LIST.load(); console.log('loaded'); STORAGE_LIST.init(); STORAGE_LIST.render(); onAfterWebCall('get_storage', function (request, response) { if (response.xhr.status !== 200) { return; } console.log('shown: ' , STORAGE_LIST.shown) if (!STORAGE_LIST.shown) { return; } STORAGE_LIST.init(); STORAGE_LIST.render(); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址