DF Storage List

Shows your storage, but as a list. Features filters and sorting.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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();
    }); 
})();