/*
=========================================================
Name            :   SelectManager
GitHub          :   https://github.com/TimRohr22/Cauldron/tree/master/SelectManager
Roll20 Contact  :   timmaugh && TheAaron
Version         :   1.1.8
Last Update     :   05 APRIL 2024
=========================================================
*/
var API_Meta = API_Meta || {};
API_Meta.SelectManager = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 };
{ try { throw new Error(''); } catch (e) { API_Meta.SelectManager.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } }

const SelectManager = (() => { //eslint-disable-line no-unused-vars
    // ==================================================
    //		VERSION
    // ==================================================
    const apiproject = 'SelectManager';
    const version = '1.1.8';
    const schemaVersion = 0.4;
    const apilogo = 'https://i.imgur.com/ewyOzMU.png';
    const apilogoalt = 'https://i.imgur.com/3U8c9rE.png'
    API_Meta[apiproject].version = version;
    const vd = new Date(1712321265957);
    const versionInfo = () => {
        log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`);
        if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) {
            log(`  > Updating ${apiproject} Schema to v${schemaVersion} <`);
            switch (state[apiproject] && state[apiproject].version) {

                case 0.1:
                    state[apiproject].settings = {
                        playerscanids: false
                    };
                    if (state[apiproject].hasOwnProperty('autoinsert')) state[apiproject].settings.autoinsert = [...state[apiproject].autoinsert];
                    else state[apiproject].settings.autoinsert = ['selected'];
                    state[apiproject].defaults = {
                        autoinsert: ['selected'],
                        playerscanids: false
                    };
                    delete state[apiproject].autoinsert;
                /* falls through */
                case 0.2:
                    state[apiproject].settings.knownsenders = ['CRL'];
                    state[apiproject].defaults.knownsenders = ['CRL'];
                /* falls through */
                case 0.3:
                    state[apiproject].settings.show04message = true;
                    state[apiproject].defaults.show04message = true;
                /* falls through */
                case 'UpdateSchemaVersion':
                    state[apiproject].version = schemaVersion;
                    break;

                default:
                    state[apiproject] = {
                        version: schemaVersion,
                        settings: {
                            autoinsert: ['selected'],
                            playerscanids: false,
                            knownsenders: ['CRL'],
                            show03message: true
                        },
                        defaults: {
                            autoinsert: ['selected'],
                            playerscanids: false,
                            knownsenders: ['CRL'],
                            show03message: true
                        }
                    };
                    break;
            }
        }
    };
    const manageState = { // eslint-disable-line no-unused-vars
        reset: () => state[apiproject].settings = _.clone(state[apiproject].defaults),
        set: (p, v) => state[apiproject].settings[p] = v,
        get: (p) => { return state[apiproject].settings[p]; }
    };

    const logsig = () => {
        // initialize shared namespace for all signed projects, if needed
        state.torii = state.torii || {};
        // initialize siglogged check, if needed
        state.torii.siglogged = state.torii.siglogged || false;
        state.torii.sigtime = state.torii.sigtime || Date.now() - 3001;
        if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) {
            const logsig = '\n' +
                '  _____________________________________________   ' + '\n' +
                '   )_________________________________________(    ' + '\n' +
                '     )_____________________________________(      ' + '\n' +
                '           ___| |_______________| |___            ' + '\n' +
                '          |___   _______________   ___|           ' + '\n' +
                '              | |               | |               ' + '\n' +
                '              | |               | |               ' + '\n' +
                '              | |               | |               ' + '\n' +
                '              | |               | |               ' + '\n' +
                '              | |               | |               ' + '\n' +
                '______________|_|_______________|_|_______________' + '\n' +
                '                                                  ' + '\n';
            log(`${logsig}`);
            state.torii.siglogged = true;
            state.torii.sigtime = Date.now();
        }
        return;
    };
    const generateUUID = (() => {
        let a = 0;
        let b = [];

        return () => {
            let c = (new Date()).getTime() + 0;
            let f = 7;
            let e = new Array(8);
            let d = c === a;
            a = c;
            for (; 0 <= f; f--) {
                e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);
                c = Math.floor(c / 64);
            }
            c = e.join("");
            if (d) {
                for (f = 11; 0 <= f && 63 === b[f]; f--) {
                    b[f] = 0;
                }
                b[f]++;
            } else {
                for (f = 0; 12 > f; f++) {
                    b[f] = Math.floor(64 * Math.random());
                }
            }
            for (f = 0; 12 > f; f++) {
                c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);
            }
            return c;
        };
    })();
    const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); };
    const RX = (() => {
        const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g, '\\$1');
        const entities = {
            '*': { detect: /\*/, rx: /\*/, rep: '.*?' },
            '?': { detect: /\?/, rx: /\?/, rep: '.' },
            //    '?': { detect: /.\?/, rx: /(.)\?/, rep: '$1?'}
        };
        const rxkeys = (k) => entities[k].detect.source;
        const getSource = (s) => {
            let rxsource = '';
            let rxflags = '';
            let ret;
            const rxpattern = /^\/(?<source>.*?)\/(?<flags>(?:g|i|m|s|u|y){0,6})$/i;
            if (rxpattern.test(s)) {
                ret = rxpattern.exec(s);
                rxsource = ret.groups.source;
                rxflags = ret.groups.flags || '';
            } else {
                rxsource = ['^',
                    ...s.split(new RegExp(`(${Object.keys(entities).map(rxkeys).join('|')})`))
                        .map(p => {
                            return Object.keys(entities).reduce((m, k) => {
                                let rx = new RegExp(`^${entities[k].rx.source}$`);
                                if (typeof m === 'undefined' && rx.test(p)) {
                                    m = p.replace(rx, entities[k].rep);
                                }
                                return m;
                            }, undefined) || esRE(p);
                        }),
                    '$'
                ].join('');
                rxflags = 'gi';
            }
            return new RegExp(rxsource, rxflags);
        };
        return getSource;
    })();
    const playersCanUseIDs = () => manageState.get('playerscanids');
    const getTheSpeaker = msg => {
        let speaking;
        if (['API', ''].includes(msg.who)) {
            speaking = { id: undefined, type: 'API', localName: 'API', speakerType: 'API', chatSpeaker: 'API', get: () => { return 'API'; } };
        } else {
            let characters = findObjs({ type: 'character' });
            characters.forEach(c => { if (c.get('name') === msg.who) speaking = c; });

            if (speaking) {
                speaking.speakerType = "character";
                speaking.localName = speaking.get("name");
            } else {
                speaking = getObj('player', msg.playerid);
                speaking.speakerType = "player";
                speaking.localName = speaking.get("displayname");
            }
            speaking.chatSpeaker = speaking.speakerType + '|' + speaking.id;
        }

        return speaking;
    };
    const playerCanControl = (obj, playerid = 'any') => {
        const playerInControlledByList = (list, playerid) => list.includes('all') || list.includes(playerid) || ('any' === playerid && list.length);
        let players = obj.get('controlledby')
            .split(/,/)
            .filter(s => s.length);

        if (playerInControlledByList(players, playerid)) {
            return true;
        }

        if ('' !== obj.get('represents')) {
            players = (getObj('character', obj.get('represents')) || { get: function () { return ''; } })
                .get('controlledby').split(/,/)
                .filter(s => s.length);
            return playerInControlledByList(players, playerid);
        }
        return false;
    };
    const getPageForPlayer = (playerid) => {
        let player = getObj('player', playerid);
        if (playerIsGM(playerid)) {
            return player.get('lastpage') || Campaign().get('playerpageid');
        }

        let psp = Campaign().get('playerspecificpages');
        if (psp[playerid]) {
            return psp[playerid];
        }

        return Campaign().get('playerpageid');
    };
    const getTokens = (query, pid, owner = true) => {
        if (pid === 'API') pid = preservedMsgObj[maintrigger].playerid;
        let pageid = getPageForPlayer(pid);
        let qrx = RX(query);
        let alltokens = [...findObjs({ type: 'graphic', pageid: pageid }), ...findObjs({ type: 'text', pageid: pageid }), ...findObjs({ type: 'path', pageid: pageid })]
            .filter(t => t.get('layer') === 'objects' || playerIsGM(pid));
        if (owner) {
            alltokens = alltokens.filter(t => playerIsGM(pid) || playersCanUseIDs() || playerCanControl(t, pid));
        }
        let tokens = [(alltokens.filter(t => t.id === query)[0] ||
            alltokens.filter(t => t.get('name') === query)[0])]
            .filter(t => t);
        if (!tokens.length) {
            tokens = alltokens.filter(t => {
                qrx.lastIndex = 0;
                return qrx.test(typeof t.get('name') === 'undefined' ? '' : t.get('name'));
            });
        }
        return tokens;
    };

    let html = {};
    let css = {}; // eslint-disable-line no-unused-vars
    let HE = () => { }; // eslint-disable-line no-unused-vars
    const theme = {
        primaryColor: '#E66B00',
        primaryTextColor: '#232323',
        primaryTextBackground: '#ededed'
    }
    const localCSS = {
        msgheader: {
            'background-color': theme.primaryColor,
            'color': 'white',
            'font-size': '1.2em',
            'padding-left': '4px'
        },
        msgbody: {
            'color': theme.primaryTextColor,
            'background-color': theme.primaryTextBackground
        },
        msgfooter: {
            'color': theme.primaryTextColor,
            'background-color': theme.primaryTextBackground
        },
        msgheadercontent: {
            'display': 'table-cell',
            'vertical-align': 'middle',
            'padding': '4px 8px 4px 6px'
        },
        msgheaderlogodiv: {
            'display': 'table-cell',
            'max-height': '30px',
            'margin-right': '8px',
            'margin-top': '4px',
            'vertical-align': 'middle'
        },
        logoimg: {
            'background-color': 'transparent',
            'float': 'left',
            'border': 'none',
            'max-height': '30px'
        },
        boundingcss: {
            'background-color': theme.primaryTextBackground
        },
        inlineEmphasis: {
            'font-weight': 'bold'
        },
        button: {
            'background-color': theme.primaryColor,
            'border-radius': '6px',
            'min-width': '25px',
            'padding': '6px 8px'
        }
    }
    const msgbox = ({
        msg: msg = '',
        title: title = '',
        headercss: headercss = localCSS.msgheader,
        bodycss: bodycss = localCSS.msgbody,
        footercss: footercss = localCSS.msgfooter,
        sendas: sendas = 'SelectManager',
        whisperto: whisperto = '',
        footer: footer = '',
        btn: btn = '',
    } = {}) => {
        if (title) title = html.div(html.div(html.img(apilogoalt, 'SelectManager Logo', localCSS.logoimg), localCSS.msgheaderlogodiv) + html.div(title, localCSS.msgheadercontent), {});
        Messenger.MsgBox({ msg: msg, title: title, bodycss: bodycss, sendas: sendas, whisperto: whisperto, footer: footer, btn: btn, headercss: headercss, footercss: footercss, boundingcss: localCSS.boundingcss, noarchive: true });
    };

    const getWhisperTo = (who) => who.toLowerCase() === 'api' ? 'gm' : who.replace(/\s\(gm\)$/i, '');
    const handleConfig = msg => {
        if (msg.type !== 'api' || !/^!smconfig/.test(msg.content)) return;
        let recipient = getWhisperTo(msg.who);
        if (!playerIsGM(msg.playerid)) {
            msgbox({ title: 'GM Rights Required', msg: 'You must be a GM to perform that operation', whisperto: recipient });
            return;
        }
        let cfgrx = /^(\+|-)(selected|who|playerid|playerscanids|acknowledge(\d+))$/i;
        let changeObj = {
            '+': 'enabled',
            '-': 'disabled',
            'a': 'acknowledged'
        };
        let res;
        let cfgTrack = {};
        let message;
        if (/^!smconfig\s+[^\s]/.test(msg.content)) {
            msg.content.split(/\s+/).slice(1).forEach(a => {
                res = cfgrx.exec(a);
                if (!res) return;
                if (res[2].toLowerCase() === 'playerscanids') {
                    manageState.set('playerscanids', (res[1] === '+'));
                    cfgTrack[res[2]] = res[1];
                } else if (['selected', 'who', 'playerid'].includes(res[2].toLowerCase())) {
                    if (res[1] === '+') {
                        manageState.set('autoinsert', [...new Set([...manageState.get('autoinsert'), res[2].toLowerCase()])]);
                        cfgTrack[res[2]] = res[1];
                    } else {
                        manageState.set('autoinsert', manageState.get('autoinsert').filter(e => e !== res[2].toLowerCase()));
                        cfgTrack[res[2]] = res[1];
                    }
                } else if (/^acknowledge\d+$/i.test(res[2])) {
                    manageState.set(`show${res[3]}message`, false);
                    cfgTrack[`Schema ${res[3]} Message`] = 'a';
                }
            });
            let changes = Object.keys(cfgTrack).map(k => `${html.span(k, localCSS.inlineEmphasis)}: ${changeObj[cfgTrack[k]]}`).join('<br>');
            msgbox({ title: `SelectManager Config Changed`, msg: `You have made the following changes to the SelectManager configuration:<br>${changes}`, whisperto: recipient });
        } else {
            cfgTrack.playerscanids = `${html.span('playerscanids', localCSS.inlineEmphasis)}: ${manageState.get('playerscanids') ? 'enabled' : 'disabled'}`;
            cfgTrack.autoinsert = ['selected', 'who', 'playerid'].map(k => `${html.span(k, localCSS.inlineEmphasis)}: ${manageState.get('autoinsert').includes(k) ? 'enabled' : 'disabled'}`).join('<br>');
            message = `SelectManager is currently configured as follows:<br>${cfgTrack.playerscanids}<br>${cfgTrack.autoinsert}`;
            msgbox({ title: 'SelectManager Configuration', msg: message, whisperto: recipient });
        }
    };

    const issueVersionUpdateMessages = () => {
        let allCommands = [...findObjs({ type: 'macro' }), ...findObjs({ type: 'ability' })];

        const show04Message = () => {
            let affected = allCommands.filter(o => {
                let cmd = o.get('action');
                let locSelrx = /{&\s*(?:select|inject)\s+([^}]+?)\s*}/gi;
                let found = false;
                let res;
                let items;
                while (!found && (res = locSelrx.exec(cmd)) && res) {
                    found = !!(res[1].split(/\s*,\s*/)
                        .filter(item => oldmarkerrx.test(item)).length);
                        // .filter(item => /^(\+|-)/.test(item) && !/^(\+|-)(@.*|#.*|\*.*|((bar|max)(1|2|3){1})|((aura|color)(1|2){0,1})|layer|tip|gmnotes|type|pc|npc|pt|side)(\s|<|>|=|~|!|$)/.test(item)).length);
                }
                return found;
            });

            if (affected.length) {
                let listAffected = affected.map(a => `<li>${a.get('name')} (${a.get('type') === 'ability' ? `ability for ${getObj('character', a.get('characterid')).get('name')}` : 'macro'})</li>`).join('');
                let message = html.p(`A small portion of SelectManager syntax is changing. A previous update made it possible to use status markers (either their presence or value) as a ` +
                    `condition for virtually selecting that token. For instance, testing a token for the presence of a status marker named "noble" would look like:<br><br><b>+noble</b>`) +
                    html.p(`This syntax allowed for "collisions" -- a situation where a marker might bear the name of one of the other keywords SelectManager looks for as ways to test the tokens: aura, bar1, npc, etc. ` +
                    `For instance, if you were playing in a game that had a status marker named "npc", then would the syntax <b>+npc</b> refer to the presence of the marker, or to the internal test ` +
                    `SelectManager uses to determine if a token is an npc?`) +
                    html.p(`With the v1.1.8 update, SelectManager can now use a similar syntax to test a token for the presence of character tags, increasing the possibility of these collisions (i.e., a tag ` +
                        `and a marker both named "noble"). Because of this, the syntax to test for a status marker is getting an update to allow for greater specificity. Going forward, ` +
                        `to test for a status marker on a token, you should simply preface the marker name with an asterisk (&ast;) immediately following the "+" (for "should have") or "-" ` +
                        `(for "should not have"):<br><br><b>+&ast;noble</b><br><b>+&ast;noble > 2</b>`) +
                    html.p(`The previous syntax is still available for now, but is no longer supported and will be removed at some point in the future. You should take a moment to update commands ` +
                        `in your game that utilize the previous construction (without an asterisk). A quick scan of character abilities and macros in this game shows that the following list ` +
                        `might be commands where you have utilized the previous syntax:` +
                        `<ul>${listAffected}</ul>`);
                //const button = ({ elem: elem = '', label: label = '', char: char = '', type: type = '%', css: css = Messenger.Css.button } = {}) => {

                let button = Messenger.Button({ elem: `!smconfig +acknowledge04`, type: '!', label: `Don't Show Again`, css: localCSS.button, noarchive: true });
                msgbox({ title: 'SelectManager Syntax Update', msg: message, whisperto: 'gm', btn: button });

                // TODO: make sure chat message has opt-out for not getting the message again
            } else {
                manageState.set('show04message', false);
            }
        };

        const messageSettings = {
            show04message: show04Message
        };

        Object.keys(messageSettings).forEach(k => {
            if (manageState.get(k)) { messageSettings[k](); }
        });
    };

    const maintrigger = `${apiproject}-main`;
    let preservedMsgObj = {
        [maintrigger]: { selected: undefined, who: '', playerid: '' }
    };

    const condensereturn = (funcret, status, notes) => {
        funcret.runloop = (status.includes('changed') || status.includes('unresolved'));
        if (status.length) {
            funcret.status = status.reduce((m, v) => {
                switch (m) {
                    case 'unchanged':
                        m = v;
                        break;
                    case 'changed':
                        m = v === 'unresolved' ? v : m;
                        break;
                    case 'unresolved':
                        break;
                }
                return m;
            });
        }
        funcret.notes = notes.join('<br>');
        return funcret;
    };
    const uniqueArrayByProp = (array, prop = 'id') => {
        const set = new Set;
        return array
            .filter(o => typeof o !== 'undefined' && !set.has(o[prop]) && set.add(o[prop]));
    };
    let oldmarkerrx;
    const decomposeStatuses = (list = '') => {
        return list.split(/\s*,\s*/g).filter(s => s.length)
            .reduce((m, s) => {
                let origst = libTokenMarkers.getStatus(s.slice(0, /(@\d+$|:)/.test(s) ? /(@\d+$|:)/.exec(s).index : s.length));
                let st = _.clone(origst);
                if (!st) return m;
                st.num = /^.+@0*(\d+)/.test(s) ? /^.+@0*(\d+)/.exec(s)[1] : '';
                st.html = origst.getHTML();
                st.url = st.url || '';
                m.push(st);
                return m;
            }, []);
    };
    class StatusBlock {
        constructor({ token: token = {}, msgId: msgId = generateUUID() } = {}) {
            this.token = token;
            this.msgId = msgId;
            this.statuses = (decomposeStatuses(token.get('statusmarkers')) || []).reduce((m, s) => {
                m[s.name] = m[s.name] || []
                m[s.name].push(Object.assign({}, s, { is: 'yes' }));
                return m;
            }, {});
        }
    }

    const tokenStatuses = {};
    const getStatus = (token, query, msgId) => {
        let rxret, status, index, modindex, statusblock;
        if (!token) return;
        // token = simpleObj(token);
        // if (token && !token.hasOwnProperty('id')) token.id = token._id;
        if (!tokenStatuses.hasOwnProperty(token.id) || tokenStatuses[token.id].msgId !== msgId) {
            tokenStatuses[token.id] = new StatusBlock({ token: token, msgId: msgId });
        }
        rxret = /(?<marker>.+?)(?:\?(?<index>\d+|all\+?))?$/.exec(query);
        [status, index] = [rxret.groups.marker, rxret.groups.index];
        if (!index) {
            modindex = 1;
        } else if (['all', 'all+'].includes(index.toLowerCase())) {
            modindex = index.toLowerCase();
        } else {
            modindex = Number(index);
        }
        statusblock = tokenStatuses[token.id].statuses[status];
        if (!statusblock || !statusblock.length) {
            return { is: 'no', count: '0' };
        };
        switch (index) {
            case 'all':
                return statusblock.reduce((m, sm) => {
                    m.num = `${m.num || ''}${sm.num}`;
                    m.tag = m.tag || sm.tag;
                    m.url = m.url || sm.url;
                    m.html = m.html || sm.html;
                    m.is = 'yes';
                    m.count = m.count || statusblock.length;
                    return m;
                }, {});
            case 'all+':
                return statusblock.reduce((m, sm) => {
                    m.num = `${Number(m.num || 0) + Number(sm.num)}`;
                    m.tag = m.tag || sm.tag;
                    m.url = m.url || sm.url;
                    m.html = m.html || sm.html;
                    m.is = 'yes';
                    m.count = m.count || statusblock.length;
                    return m;
                }, {});
            default:
                if (statusblock.length >= modindex) {
                    return Object.assign({}, statusblock[modindex - 1], { count: index ? '1' : statusblock.length });
                } else {
                    return { is: 'no', 'count': '0' };
                }
        }
    };

    const checkTicks = (s, check = ["'", "`", '"']) => {
        if (typeof s !== 'string') return s;
        return ((s.charAt(0) === s.charAt(s.length - 1)) && check.includes(s.charAt(0))) ? s.slice(1, s.length - 1) : s;
    };
    const isPlayerToken = (obj = { get: () => { return undefined; } }, pc = false) => {
        let players;
        if (!pc) {
            players = obj.get('controlledby')
            .split(/,/)
            .filter(s => s.length);

            if (players.includes('all') || players.filter((p) => !playerIsGM(p)).length) {
                return true;
            }
        }

        if ('' !== obj.get('represents')) {
            players = (getObj('character', obj.get('represents')) || { get: function () { return ''; } })
                .get('controlledby')
                .split(/,/)
                .filter(s => s.length);
            return !!(players.includes('all') || players.filter((p) => !playerIsGM(p)).length);
        }
        return false;
    };
    const isNPC = (obj = { get: () => { return undefined; } }) => {
        let players = (
            obj.get('represents') && obj.get('represents').length
                ? getObj('character', obj.get('represents') || { get: function () { return ''; } })
                : obj
        )
            .get('controlledby').split(/,/)
            .filter(s => s.length && !playerIsGM(s));
        return !players.length;
    };
    const internalTestLib = {
        'int': (v) => +v === +v && parseInt(parseFloat(v, 10), 10) == v,
        'num': (v) => +v === +v,
        'tru': (v) => v == true
    };
    const typeProcessor = {
        '=': (t) => t[0] == t[1],
        '!=': (t) => t[0] != t[1],
        '~': (t) => t[0].includes(t[1]),
        '!~': (t) => !t[0].includes(t[1]),
        '>': (t) => (internalTestLib.num(t[0]) ? Number(t[0]) : t[0]) > (internalTestLib.num(t[1]) ? Number(t[1]) : t[1]),
        '>=': (t) => (internalTestLib.num(t[0]) ? Number(t[0]) : t[0]) >= (internalTestLib.num(t[1]) ? Number(t[1]) : t[1]),
        '<': (t) => (internalTestLib.num(t[0]) ? Number(t[0]) : t[0]) < (internalTestLib.num(t[1]) ? Number(t[1]) : t[1]),
        '<=': (t) => (internalTestLib.num(t[0]) ? Number(t[0]) : t[0]) <= (internalTestLib.num(t[1]) ? Number(t[1]) : t[1]),
        'in': (t) => {
            let array = (/^\[?([^\]]+)\]?$/.exec(t[1])[1] || '').split(/\s*,\s*/);
            return array.includes(t[0]);
        }
    }

    const evaluateCriteria = (c, t, msgId) => {
        let comp = [];
        let tksetting;
        let test = c.test;
        let attrret = 'current'; // current or max
        let attrval;
        let attrres;
        switch (c.type) {
            case 'bar':
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [t.get(`bar${['1', '2', '3'].includes(c.ident) ? c.ident : '1'}_value`), c.value];
                }
                break;
            case 'max':
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [t.get(`bar${['1', '2', '3'].includes(c.ident) ? c.ident : '1'}_max`), c.value];
                }
                break;
            case 'aura':
                if (test && test.length && c.value && !isNaN(c.value) && typeProcessor.hasOwnProperty(test)) { // testing radius of aura
                    tksetting = t.get(`aura${['1', '2'].includes(c.ident) ? c.ident : '1'}_radius`);
                    if (tksetting && tksetting.length) {
                        comp = [tksetting, c.value];
                    }
                } else { // testing presence of aura
                    tksetting = t.get(`aura${['1', '2'].includes(c.ident) ? c.ident : '1'}_radius`);
                    comp = [tksetting && tksetting.length > 0, true];
                    test = '=';
                }
                break;
            case 'color':
                if (typeProcessor.hasOwnProperty(test)) {
                    tksetting = t.get(`aura${['1', '2'].includes(c.ident) ? c.ident : '1'}_radius`);
                    if (tksetting && tksetting.length) {
                        comp = [t.get(`aura${['1', '2'].includes(c.ident) ? c.ident : '1'}_color`), c.value];
                    }
                }
                break;
            case 'gmnotes':
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [t.get(`gmnotes`), c.value];
                }
                break;
            case 'tip':
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [t.get(`tooltip`), c.value];
                }
                break;
            case 'layer':
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [t.get(`layer`), c.value];
                }
                break;
            case 'marker':
                tksetting = getStatus(t, c.ident, msgId);
                if (typeProcessor.hasOwnProperty(test)) {
                    comp = [tksetting.num, c.value];
                } else { // testing presence of marker
                    test = '=';
                    comp = [tksetting.is === 'yes', true];
                }
                break;
            case 'tag':
                if (t.get('represents') && t.get('represents').length) {
                    let char = getObj('character', t.get('represents'));
                    if (char) { // testing presence of attribute
                        tksetting = JSON.parse(char.get('tags'));
                        test = '=';
                        comp = [tksetting.includes(c.ident), true];
                    }
                }
                break;
            case 'attribute':
                if (t.get('represents') && t.get('represents').length) {
                    attrres = /^(?<attr>[^.|#?]+?)(?:(?:\.|\?|#|\|)(?<attrval>current|cur|c|max|m))?\s*$/i.exec(c.ident);
                    if (attrres.groups && attrres.groups.attrval && attrres.groups.attrval.length && ['max', 'm'].includes(attrres.groups.attrval)) {
                        attrret = 'max';
                    }
                    if (typeProcessor.hasOwnProperty(test)) {
                        attrval = (findObjs({ type: 'attribute', characterid: t.get('represents') }).filter(a => a.get('name') === attrres.groups.attr)[0] || { get: () => { return '' } }).get(attrret) || '';
                        comp = [attrval, c.value];
                    } else { // testing presence of attribute
                        test = '=';
                        comp = [findObjs({ type: 'attribute', characterid: t.get('represents') }).filter(a => a.get('name') === attrres.groups.attr).length > 0, true];
                    }
                }
                break;
            case 'type':
                if (typeProcessor.hasOwnProperty(test)) {
                    if (c.value === 'graphic') {
                        tksetting = t.get('type');
                    } else {
                        tksetting = t.get('type') === 'graphic' ? t.get('subtype') : t.get('type');
                    }
                    comp = [tksetting, c.value];
                }
                break;
            case 'pc':
                if (t.get('type') === 'graphic' && t.get('subtype') === 'token' && t.get('layer') === 'objects') {
                    test = '=';
                    comp = [isPlayerToken(t, true), true];
                }
                break;
            case 'npc':
                if (t.get('type') === 'graphic' && t.get('subtype') === 'token') {
                    test = '=';
                    comp = [isNPC(t), true];
                }
                break;
            case 'pt':
                if (t.get('type') === 'graphic' && t.get('subtype') === 'token' && t.get('layer') === 'objects') {
                    test = '=';
                    comp = [isPlayerToken(t, true), false];
                }
                break;
            case 'side':
                if (typeProcessor.hasOwnProperty(test) && t.get('type') === 'graphic') {
                    tksetting = t.get('currentSide');
                    comp = [tksetting, c.value];
                }
                break;
            default:
                return false;
        }
        if (!comp.length) return false;
        let result = typeProcessor[test](comp);
        return c.musthave ? result : !result;
    };

    class Criteria {
        constructor({
            type: type = '',
            musthave: musthave = '',
            ident: ident = '',
            test: test = '',
            value: value = ''
        } = {}) {
            this.type = type;
            this.musthave = musthave;
            this.ident = ident;
            this.test = test;
            this.value = value;
        }
    }
    const injectrx = /(\()?{&\s*inject\s+([^}]+?)\s*}((?<=\({&\s*inject\s+([^}]+?)\s*})\)|\1)/gi;
    const selectrx = /(\()?{&\s*select\s+([^}]+?)\s*}((?<=\({&\s*select\s+([^}]+?)\s*})\)|\1)/gi;
    const criteriarx = /^(?<musthave>\+|-)(?<attr>@|\*|#)?(?<typeitem>[^\s><=!~]+)(?:\s*$|\s*(?<test>>=|<=|~|!~|=|!=|<|>|in(?=\s+\[[^\]]+\]))\s*(?<value>.+)$)/;
    const typeitemrx = /^(?<type>bar|max|aura|color|layer|tip|gmnotes|type|pc|npc|pt|side)(?<ident>1|2|3)?(?<!bar|max|aura3|color3|layer1|layer2|layer3|tip1|tip2|tip3|gmnotes1|gmnotes2|gmnotes3|type1|type2|type3|pc1|pc2|pc3|npc1|npc2|npc3|pt1|pt2|pt3|side1|side2|side3)$/i;
    const inject = (msg, status, msgId/*, notes*/) => {
        const layerCriteria = (criteria) => {
            return criteria.filter(c => c.type === 'layer').length ? true : false;
        };
        const caseLibrary = [
            { rx: /^(\+|-)[^\s]+\s+in\s+\[$/i, terminator: ']' }
        ];
        const getGroups = (cmd, index = 0, groups = []) => {
            const getNextGroup = (cmd, terminator = ',') => {
                let s = '';
                let bstop = false;
                while (index <= cmd.length - 1 && !bstop) {
                    if (cmd.charAt(index) === terminator) {
                        if (terminator !== ',') {
                            s = `${s}${terminator}`;
                            index++;
                        }
                        bstop = true;
                    } else {
                        if (s.length || cmd.charAt(index) !== ' ') {
                            s = `${s}${cmd.charAt(index)}`;
                        }
                        index++;
                        for (const c of caseLibrary) {
                            c.rx.lastIndex = 0;
                            if (c.rx.test(s)) {
                                s = `${s}${getNextGroup(cmd, c.terminator)}`;
                            }
                        }
                    }
                }
                return s;
            };
            while (index <= cmd.length - 1) {
                groups.push(getNextGroup(cmd));
                index++;
            }
            return groups;
        };
        const unpackGroups = (array) => {
            return array
                .map(l => getTokens(l, msg.playerid))
                .reduce((m, group) => {
                    m = [...m, ...group];
                    return m;
                }, [])
                .filter(t => typeof t !== 'undefined');
        };
        const replaceOps = (rx, rxtype) => {
            rx.lastIndex = 0;
            msg.content = msg.content.replace(rx, (m, padding, group) => {
                if (rxtype === 'inject') {
                    msg.selected = msg.selected || [];
                } else if (rxtype === 'select') {
                    msg.selected = [];
                }
                let identifiers = getGroups(group)
                    .reduce((m, v) => {
                        if (criteriarx.test(v) && !findObjs({ id: v }).length) {
                            let critres = criteriarx.exec(v);
                            let newcriteria = new Criteria({ musthave: (critres.groups.musthave === '+'), test: (critres.groups.test || ''), value: checkTicks((critres.groups.value || '')) });
                            if (critres.groups.attr && critres.groups.attr === '@') {
                                newcriteria.type = 'attribute';
                                newcriteria.ident = (critres.groups.typeitem || '');
                            } else if (critres.groups.attr && critres.groups.attr === '*') {
                                newcriteria.type = 'marker';
                                newcriteria.ident = (critres.groups.typeitem || '');
                            } else if (critres.groups.attr && critres.groups.attr === '#') {
                                newcriteria.type = 'tag';
                                newcriteria.ident = (critres.groups.typeitem || '');
                            } else if (typeitemrx.test(critres.groups.typeitem)) {
                                let ti_res = typeitemrx.exec(critres.groups.typeitem);
                                newcriteria.type = ti_res.groups.type;
                                newcriteria.ident = ti_res.groups.ident;
                            } else if (oldmarkerrx.test(v)) {
                                newcriteria.type = 'marker';
                                newcriteria.ident = critres.groups.typeitem;
                            } else {
                                m.selections.push(v);
                            }
                            m.criteria.push(newcriteria);
                        } else {
                            m.selections.push(v);
                        }
                        return m;
                    }, { criteria: [], selections: [] });
                if (playerIsGM(msg.playerid) && !layerCriteria(identifiers.criteria)) {
                    identifiers.criteria.push(new Criteria({ type: 'layer', musthave: true, test: '=', value: 'objects' }));
                }
                identifiers.selections = uniqueArrayByProp(unpackGroups(identifiers.selections), 'id')
                    .filter(t => {
                        return identifiers.criteria.every(c => evaluateCriteria(c, t, msgId));
                    });

                msg.selected = identifiers.selections
                    .map(t => { return { '_id': t.id, '_type': t.get('type') }; })
                    .reduce((m, t) => {
                        if (!m.map(mt => mt._id).includes(t._id)) {
                            m.push(t);
                        }
                        return m;
                    }, msg.selected);

                status.push('changed');
                return '';
            });
        };
        let retResult = false;
        // handle selections
        if (selectrx.test(msg.content)) {
            retResult = true;
            replaceOps(selectrx, 'select');
        }
        // handle injections
        if (injectrx.test(msg.content)) {
            retResult = true;
            replaceOps(injectrx, 'inject');
        }
        if (msg.selected && !msg.selected.length) delete msg.selected;
        return retResult;
    };

    const dispatchForSelected = (trigger, i) => {
        if (preservedMsgObj[trigger].selected.length > i) {
            sendChat(preservedMsgObj[trigger].chatSpeaker, `!${trigger}${i} ${preservedMsgObj[trigger].dsmsg.replace(/{&\s*i\s*((\+|-)\s*([\d]+)){0,1}}/gi, ((m, g1, op, val) => { return !g1 ? i : op === '-' ? parseInt(i) - parseInt(val) : parseInt(i) + parseInt(val); }))}`);
        }
        if (preservedMsgObj[trigger].selected.length <= i + 1) {
            setTimeout(() => { delete preservedMsgObj[trigger] }, 10000);
        }
    };
    const fsrx = /(^!forselected(--|\+\+|\+-|-\+|\+|-|)(?:\((.)\)){0,1}(-silent)?\s+!?).+/i;
    const forselected = (msg, apitrigger) => {
        apitrigger = `${apiproject}${generateUUID()}`;
        if (!(preservedMsgObj[maintrigger].selected && preservedMsgObj[maintrigger].selected.length)) {
            let fsres = fsrx.exec(msg.content);
            if (fsres && !fsres[4]) { // account for silent output
                msgbox({ msg: `No selected tokens to use for that command. Please select some tokens then try again.`, title: `NO TOKENS`, whisperto: getWhisperTo(preservedMsgObj[maintrigger].who) });
            }
            return;
        }
        preservedMsgObj[apitrigger] = {
            selected: [...(preservedMsgObj[maintrigger].selected || [])],
            who: preservedMsgObj[maintrigger].who,
            playerid: preservedMsgObj[maintrigger].playerid,
            dsmsg: ''
        };
        preservedMsgObj[apitrigger].chatSpeaker = getTheSpeaker(preservedMsgObj[apitrigger]).chatSpeaker;
        let fsres = fsrx.exec(msg.content);
        switch (fsres[2] || '++') {
            case '+-':
                preservedMsgObj[apitrigger].replaceid = true;
                preservedMsgObj[apitrigger].replacename = false;
                break;
            case '-':
            case '-+':
                preservedMsgObj[apitrigger].replaceid = false;
                preservedMsgObj[apitrigger].replacename = true;
                preservedMsgObj[apitrigger].nametoreplace = findObjs({ id: preservedMsgObj[apitrigger].selected[0]._id })[0].get('name');
                break;
            case '--':
                preservedMsgObj[apitrigger].replaceid = false;
                preservedMsgObj[apitrigger].replacename = false;
                break;
            case '+':
            case '++':
            default:
                preservedMsgObj[apitrigger].replaceid = true;
                preservedMsgObj[apitrigger].replacename = true;
                preservedMsgObj[apitrigger].nametoreplace = findObjs({ id: preservedMsgObj[apitrigger].selected[0]._id })[0].get('name');
                break;
        }
        msg.content = msg.content.replace(/<br\/>\n/g, ' ');
        preservedMsgObj[apitrigger].dsmsg = msg.content.slice(fsres[1].length);
        if (fsres[3]) {
            preservedMsgObj[apitrigger].dsmsg = preservedMsgObj[apitrigger].dsmsg.replace(new RegExp(escapeRegExp(fsres[3]), 'g'), '');
        }
        dispatchForSelected(apitrigger, 0);
        //preservedMsgObj[apitrigger].selected.forEach((t, i) => {
        //    sendChat(chatSpeaker, `!${apitrigger}${i} ${dsmsg.replace(/{&\s*i\s*((\+|-)\s*([\d]+)){0,1}}/gi, ((m, g1, op, val) => { return !g1 ? i : op === '-' ? parseInt(i) - parseInt(val) : parseInt(i) + parseInt(val); }))}`);
        //});
        //setTimeout(() => { delete preservedMsgObj[apitrigger] }, 10000);
    };
    const trackprops = (msg) => {
        [
            preservedMsgObj[maintrigger].who,
            preservedMsgObj[maintrigger].selected,
            preservedMsgObj[maintrigger].playerid,
            preservedMsgObj[maintrigger].inlinerolls
        ] = [msg.who, msg.selected, msg.playerid, msg.inlinerolls];
    };
    const handleInput = (msg, msgstate = {}) => {
        let funcret = { runloop: false, status: 'unchanged', notes: '' };
        const trigrx = new RegExp(`^!(${Object.keys(preservedMsgObj).join('|')})`);
        let apitrigger; // the apitrigger used by the message
        if (!Object.keys(msgstate).length && scriptisplugin) return funcret;
        let status = [];
        let notes = [];
        let msgId = generateUUID();
        msg.content = msg.content.replace(/<br\/>\n/g, '({&br-sm})');
        let injection = inject(msg, status, msgId, notes);
        if ('API' !== msg.playerid) { // user generated message
            trackprops(msg);
        } else { // API generated message
            if (injection) preservedMsgObj[maintrigger].selected = msg.selected;
            // peel off ZeroFrame trigger, if it's there
            if (msg.apitrigger) msg.content = msg.content.replace(msg.apitrigger, '');
            if (trigrx.test(msg.content)) { // message has apitrigger (iterative call of forselected) so cycle-in next selected
                apitrigger = trigrx.exec(msg.content)[1];
                msg.content = msg.content.replace(apitrigger, '');
                status.push('changed');
                let nextindex = /^!(\d+)\s*/.exec(msg.content)[1];
                msg.content = `!${msg.content.slice(nextindex.length + 2)}`;
                nextindex = Number(nextindex);
                msg.selected = [];
                msg.selected.push(preservedMsgObj[apitrigger].selected[nextindex]);
                msg.who = preservedMsgObj[apitrigger].who;
                msg.playerid = preservedMsgObj[apitrigger].playerid;
                // handle replacements of @{selected|token_id} and @{selected|token_name}
                if (preservedMsgObj[apitrigger].replaceid) {
                    msg.content = msg.content.replace(apitrigger, '').replace(preservedMsgObj[apitrigger].selected[0]._id, msg.selected[0]._id);
                }
                if (preservedMsgObj[apitrigger].replacename && preservedMsgObj[apitrigger].nametoreplace && msg.selected[0]._type === 'graphic') {
                    msg.content = msg.content.replace(apitrigger, '').replace(preservedMsgObj[apitrigger].nametoreplace, findObjs({ id: msg.selected[0]._id })[0].get('name'));
                }
                // handle replacements of at{selected|prop}
                if (typeof Fetch !== 'undefined' && typeof ZeroFrame !== 'undefined') {
                    const fetchselrx = /at\((?<token>selected)[|.](?<item>[^\s[|.)]+?)(?:[|.](?<valtype>[^\s.[|]+?)){0,1}(?:\[(?<default>[^\]]*?)]){0,1}\s*\)/gi;
                    const fetchrptgselrx = /at\((?<character>selected)[|.](?<section>[^\s.|]+?)[|.]\[\s*(?<pattern>.+?)\s*]\s*[|.](?<valuesuffix>[^[\s).]+?)(?:[|.](?<valtype>[^\s.[)]+?)){0,1}(?:\[(?<default>[^\]]*?)]){0,1}\s*\)/gi;
                    msg.content = msg.content.replace(fetchselrx, m => {
                        status.push('changed')
                        return `@${m.slice(2)}`;
                    });
                    msg.content = msg.content.replace(fetchrptgselrx, m => {
                        status.push('changed')
                        return `*${m.slice(2)}`;
                    });
                } else {
                    let selrx = /at{selected(?:\||\.)([^|}]+)(\|max)?}/ig;
                    let retval;
                    msg.content = msg.content.replace(selrx, (g0, g1, g2) => {
                        if (['token_id', 'token_name', 'bar1', 'bar2', 'bar3'].includes(g1.toLowerCase())) {
                            let tok = findObjs({ id: msg.selected[0]._id })[0];
                            if (g1.toLowerCase() === 'token_id') retval = tok.id;
                            else if (g1.toLowerCase() === 'token_name') retval = tok.get('name');
                            else retval = tok.get(`${g1}_${g2 ? 'max' : 'value'}`) || '';
                        } else {
                            let character = findObjs({ type: 'character', id: (getObj("graphic", msg.selected[0]._id) || { get: () => { return "" } }).get("represents") })[0];
                            if (!character) {
                                notes.push('No character found represented by token ${msg.selected[0]._id}');
                                status.push('unresolved');
                                retval = '';
                            } else if ('character_id' === g1.toLowerCase()) {
                                retval = character.id;
                            } else if ('character_name' === g1.toLowerCase()) {
                                retval = character.get('name');
                            }
                            status.push('changed');
                            retval(findObjs({ type: 'attribute', characterid: character.id })[0] || { get: () => { return '' } }).get(g2 ? 'max' : 'current') || '';
                        }
                    });
                }
                dispatchForSelected(apitrigger, nextindex + 1);
            } else { // api generated call to another script, copy in the appropriate data
                if (manageState.get('autoinsert').includes('selected')) {
                    if (preservedMsgObj[maintrigger].selected && preservedMsgObj[maintrigger].selected.length) {
                        msg.selected = preservedMsgObj[maintrigger].selected;
                    }
                    if (!msg.selected || (msg.selected && !msg.selected.length)) {
                        delete msg.selected;
                    }
                }
                if (manageState.get('autoinsert').includes('who') && !manageState.get('knownsenders').includes(msg.who)) {
                    msg.who = preservedMsgObj[maintrigger].who;
                }
                if (manageState.get('autoinsert').includes('playerid')) {
                    msg.playerid = preservedMsgObj[maintrigger].playerid;
                }
            }
            // replace ZeroFrame trigger, if it's there
            if (msg.apitrigger) msg.content = `!${msg.apitrigger}${msg.content.slice(1)}`;
        }
        msg.content = msg.content.replace(/\({&br-sm}\)/g, '<br/>\n');
        return condensereturn(funcret, status, notes);
    };
    const handleForSelected = (msg) => {
        if (msg.type !== 'api' || !fsrx.test(msg.content)) return;
        forselected(msg);
    };
    const getProp = (prop) => {
        return preservedMsgObj[maintrigger][prop] || undefined;
    };
    const getSelected = () => getProp('selected');
    const getWho = () => getProp('who');
    const getPlayerID = () => getProp('playerid');

    const checkDependencies = (deps) => {
        /* pass array of objects like
            { name: 'ModName', version: '#.#.#' || '', mod: ModName || undefined, checks: [ [ExposedItem, type], [ExposedItem, type] ] }
        */
        const dependencyEngine = (deps) => {
            const versionCheck = (mv, rv) => {
                let modv = [...mv.split('.'), ...Array(4).fill(0)].slice(0, 4);
                let reqv = [...rv.split('.'), ...Array(4).fill(0)].slice(0, 4);
                return reqv.reduce((m, v, i) => {
                    if (m.pass || m.fail) return m;
                    if (i < 3) {
                        if (parseInt(modv[i]) > parseInt(reqv[i])) m.pass = true;
                        else if (parseInt(modv[i]) < parseInt(reqv[i])) m.fail = true;
                    } else {
                        // all betas are considered below the release they are attached to
                        if (reqv[i] === 0 && modv[i] === 0) m.pass = true;
                        else if (modv[i] === 0) m.pass = true;
                        else if (reqv[i] === 0) m.fail = true;
                        else if (parseInt(modv[i].slice(1)) >= parseInt(reqv[i].slice(1))) m.pass = true;
                    }
                    return m;
                }, { pass: false, fail: false }).pass;
            };

            let result = { passed: true, failures: {}, optfailures: {} };
            deps.forEach(d => {
                let failObj = d.optional ? result.optfailures : result.failures;
                if (!d.mod) {
                    if (!d.optional) result.passed = false;
                    failObj[d.name] = 'Not found';
                    return;
                }
                if (d.version && d.version.length) {
                    if (!(API_Meta[d.name].version && API_Meta[d.name].version.length && versionCheck(API_Meta[d.name].version, d.version))) {
                        if (!d.optional) result.passed = false;
                        failObj[d.name] = `Incorrect version. Required v${d.version}. ${API_Meta[d.name].version && API_Meta[d.name].version.length ? `Found v${API_Meta[d.name].version}` : 'Unable to tell version of current.'}`;
                        return;
                    }
                }
                d.checks.reduce((m, c) => {
                    if (!m.passed) return m;
                    let [pname, ptype] = c;
                    if (!d.mod.hasOwnProperty(pname) || typeof d.mod[pname] !== ptype) {
                        if (!d.optional) m.passed = false;
                        failObj[d.name] = `Incorrect version.`;
                    }
                    return m;
                }, result);
            });
            return result;
        };
        let depCheck = dependencyEngine(deps);
        let failures = '', contents = '', msg = '';
        if (Object.keys(depCheck.optfailures).length) { // optional components were missing
            failures = Object.keys(depCheck.optfailures).map(k => `&bull; <code>${k}</code> : ${depCheck.optfailures[k]}`).join('<br>');
            contents = `<span style="font-weight: bold">${apiproject}</span> utilizies one or more other scripts for optional features, and works best with those scripts installed. You can typically find these optional scripts in the 1-click Mod Library:<br>${failures}`;
            msg = `<div style="width: 100%;border: none;border-radius: 0px;min-height: 60px;display: block;text-align: left;white-space: pre-wrap;overflow: hidden"><div style="font-size: 14px;font-family: &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Helvetica Neue&quot;, sans-serif"><div style="background-color: #000000;border-radius: 6px 6px 0px 0px;position: relative;border-width: 2px 2px 0px 2px;border-style:  solid;border-color: black;"><div style="border-radius: 18px;width: 35px;height: 35px;position: absolute;left: 3px;top: 2px;"><img style="background-color: transparent ; float: left ; border: none ; max-height: 40px" src="${typeof apilogo !== 'undefined' ? apilogo : 'https://i.imgur.com/kxkuQFy.png'}"></div><div style="background-color: #c94d4d;font-weight: bold;font-size: 18px;line-height: 36px;border-radius: 6px 6px 0px 0px;padding: 4px 4px 0px 43px;color: #ffffff;min-height: 38px;">MISSING MOD DETECTED</div></div><div style="background-color: white;padding: 4px 8px;border: 2px solid #000000;border-bottom-style: none;color: #404040;">${contents}</div><div style="background-color: white;text-align: right;padding: 4px 8px;border: 2px solid #000000;border-top-style: none;border-radius: 0px 0px 6px 6px"></div></div></div>`;
            sendChat(apiproject, `/w gm ${msg}`);
        }
        if (!depCheck.passed) {
            failures = Object.keys(depCheck.failures).map(k => `&bull; <code>${k}</code> : ${depCheck.failures[k]}`).join('<br>');
            contents = `<span style="font-weight: bold">${apiproject}</span> requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:<br>${failures}`;
            msg = `<div style="width: 100%;border: none;border-radius: 0px;min-height: 60px;display: block;text-align: left;white-space: pre-wrap;overflow: hidden"><div style="font-size: 14px;font-family: &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Helvetica Neue&quot;, sans-serif"><div style="background-color: #000000;border-radius: 6px 6px 0px 0px;position: relative;border-width: 2px 2px 0px 2px;border-style:  solid;border-color: black;"><div style="border-radius: 18px;width: 35px;height: 35px;position: absolute;left: 3px;top: 2px;"><img style="background-color: transparent ; float: left ; border: none ; max-height: 40px" src="${typeof apilogo !== 'undefined' ? apilogo : 'https://i.imgur.com/kxkuQFy.png'}"></div><div style="background-color: #c94d4d;font-weight: bold;font-size: 18px;line-height: 36px;border-radius: 6px 6px 0px 0px;padding: 4px 4px 0px 43px;color: #ffffff;min-height: 38px;">MISSING MOD DETECTED</div></div><div style="background-color: white;padding: 4px 8px;border: 2px solid #000000;border-bottom-style: none;color: #404040;">${contents}</div><div style="background-color: white;text-align: right;padding: 4px 8px;border: 2px solid #000000;border-top-style: none;border-radius: 0px 0px 6px 6px"></div></div></div>`;
            sendChat(apiproject, `/w gm ${msg}`);
            return false;
        }
        return true;
    };


    let scriptisplugin = false;
    const selectmanager = (m, s) => handleInput(m, s);
    on('chat:message', handleInput);
    setTimeout(() => { on('chat:message', handleForSelected) }, 0);
    on('ready', () => {
        versionInfo();
        logsig();
        let reqs = [
            {
                name: 'libTokenMarkers',
                version: `0.1.2`,
                mod: typeof libTokenMarkers !== 'undefined' ? libTokenMarkers : undefined,
                checks: [['getStatus', 'function'], ['getStatuses', 'function'], ['getOrderedList', 'function']]
            },
            {
                name: 'Messenger',
                version: `1.0.0`,
                mod: typeof Messenger !== 'undefined' ? Messenger : undefined,
                checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function'], ['Css', 'function']]
            }
        ];
        if (!checkDependencies(reqs)) return;
        html = Messenger.Html();
        css = Messenger.Css();
        HE = Messenger.HE;

        oldmarkerrx = new RegExp(`^(\\+|-)(${libTokenMarkers.getOrderedList().map(o => o.name).join('|')})`);

        issueVersionUpdateMessages();

        scriptisplugin = (typeof ZeroFrame !== `undefined`);
        if (typeof ZeroFrame !== 'undefined') {
            ZeroFrame.RegisterMetaOp(selectmanager, { priority: 20, handles: ['sm'] });
        }
        on('chat:message', handleConfig);
    });

    return { // public interface
        GetSelected: getSelected,
        GetWho: getWho,
        GetPlayerID: getPlayerID
    };

})();
{ try { throw new Error(''); } catch (e) { API_Meta.SelectManager.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.SelectManager.offset); } }
/* */