Greasy Fork is available in English.

DF Storage List

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