From f27d4309894d0b078c189a827116a4e728027d69 Mon Sep 17 00:00:00 2001 From: timmaugh Date: Sat, 27 Dec 2025 12:13:12 -0500 Subject: [PATCH] Send To Chat Functional Updates Syntax required for certain functions in a Send To Chat Reaction --- Inspector/1.0.4/Inspector.js | 1373 +++++++++++++++++++++++++ Inspector/Inspector.js | 12 +- Inspector/script.json | 5 +- SelectManager/1.1.13/SelectManager.js | 1161 +++++++++++++++++++++ SelectManager/SelectManager.js | 53 +- SelectManager/script.json | 5 +- 6 files changed, 2583 insertions(+), 26 deletions(-) create mode 100644 Inspector/1.0.4/Inspector.js create mode 100644 SelectManager/1.1.13/SelectManager.js diff --git a/Inspector/1.0.4/Inspector.js b/Inspector/1.0.4/Inspector.js new file mode 100644 index 000000000..5484da84d --- /dev/null +++ b/Inspector/1.0.4/Inspector.js @@ -0,0 +1,1373 @@ +/* +========================================================= +Name : Inspector +GitHub : +Roll20 Contact : timmaugh +Version : 1.0.4 +Last Update : 27 DEC 2025 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.Inspector = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.Inspector.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } + +// TODO: Can images in the return be detected and shown? Maybe in a pop-up? + +const Inspector = (() => { // eslint-disable-line no-unused-vars + const apiproject = 'Inspector'; + const apilogo = `https://i.imgur.com/N9swrPX.png`; // black for light backgrounds + const apilogoalt = `https://i.imgur.com/xFOQhK5.png`; // white for dark backgrounds + const version = '1.0.4'; + const schemaVersion = 0.1; + API_Meta[apiproject].version = version; + const vd = new Date(1766852906054); + 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}`); + }; + 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; + }; + // ================================================== + // STATE MANAGEMENT + // ================================================== + const checkInstall = () => { + if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { + log(` > Updating ${apiproject} Schema to v${schemaVersion} <`); + switch (state[apiproject] && state[apiproject].version) { + + case 0.1: + /* falls through */ + + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + settings: { + playersCanIDs: false, + playersCanUse: false + }, + defaults: { + playersCanIDs: false, + playersCanUse: false + }, + version: schemaVersion + } + break; + } + } + }; + let stateReady = false; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + const manageState = { // eslint-disable-line no-unused-vars + reset: () => state[apiproject].settings = _.clone(state[apiproject].defaults), + clone: () => { return _.clone(state[apiproject].settings); }, + set: (p, v) => state[apiproject].settings[p] = v, + get: (p) => { return state[apiproject].settings[p]; } + }; + const trueTypes = ['true', 't', 'yes', 'y', 'yep', 'yup', '+', 'keith', true]; + const propSanitation = (p, v) => { + const propTypes = { + 'playersCanIDs': (p, v) => validateBoolean(p, v), + 'playersCanUse': (p, v) => validateBoolean(p, v) + }; + const validateBoolean = (p, v) => { + return { prop: p, val: trueTypes.includes(v) }; + }; + + return Object.keys(propTypes).reduce((m, k) => { + if (m) return m; + if (k.toLowerCase() === p.toLowerCase()) return propTypes[k](k, v); + }, undefined); + + }; + // ================================================== + // PRESENTATION + // ================================================== + let html = {}; + let css = {}; // eslint-disable-line no-unused-vars + let HE = () => { }; + const syntaxHighlight = (obj, replacer = undefined, msgobj = {}) => { + const css = { + stringstyle: 'darkcyan;', + numberstyle: 'magenta;', + booleanstyle: 'orangered;', + nullstyle: 'darkred;', + keystyle: 'black;' + }; + let str = ''; + if (typeof obj !== 'string') { + str = JSON.stringify(obj, replacer, ' '); + obj = simpleObj(obj); + } else { + str = obj; + obj = JSON.parse(obj); + } + str = str.replace(/&/g, '&').replace(//g, '>'); + let olinkrx = new RegExp(`(${getAllObjs().map(o => o.id).join('|')})`, 'g'); + return str.replace(/\\n(? `${g1}${g2}`)).replace(/\\(.)/g, `$1`); + content = HE(content) + .replace(/\*/g, '*') + .replace(/((#[0-9A-Fa-f]{6}\d{2})|(#[0-9A-Fa-f]{6})|(#[0-9A-Fa-f]{3}))(?:.|$)(? getTipForColor(m)) + .replace(olinkrx, (m, g1) => { + let b = Messenger.Button({ type: '!', elem: `!about --${g1}`, label: 's', css: localCSS.inlineLink }); + let o = fuzzyGet(g1, msgobj, true); + let idTip = ''; + if (o && o.obj && o.obj.length) { + o = o.obj[0]; + idTip = getTipFromObjForID(o); + } else { + o = undefined; + } + // idTip = getTipFromObjForID(o); + return idTip ? idTip.replace(`${g1}`, `${g1}${b}`) : `${g1}${b}`; + }); + return `${content}`; + }) + .replace(/gmnotes:<\/span>/, () => { + if (obj && obj.gmnotes && obj.gmnotes.length) { + return `${getTip(decodeURIComponent(decodeUnicode(obj.gmnotes)), 'gmnotes', 'GM Notes')}`; + } + }).replace(/(>statusmarkers:<\/span>\s*]*>)(.*?)(<\/span>)/g, (m, pretag, list, posttag) => { + let newlist = list.split(/\s*,\s*/).map(sm => { + let tagres = /([^&:]*?)(?:@|:|$)/.exec(sm); + let name = tagres[1]; + //let ltmret = libTokenMarkers.getStatus(name); + return getTip(libTokenMarkers.getStatus(name).getHTML(5).replace(/div/gi, 'span'), sm, name, { 'text-align': 'center' }); + }).join(', '); + return `${pretag}${newlist}${posttag}`; + }) + .replace(new RegExp(msgobj.aboutUUID, 'g'), '
'); + }; + const showObjInfo = ({ + o: o = '', + title: title = 'PARSED OBJECT', + replacer: replacer = undefined, + sendas: sendas = "Inspector", + whisperto: whisperto = "", + headercss: headercss = {}, + bodycss: bodycss = {}, + msgobj: msgobj = {} + } = {}) => { + let buttons = ''; + if (libButtonsForRelatedChildren.hasOwnProperty(o._type)) { + buttons = html.div(Object.keys(libButtonsForRelatedChildren[o._type]).map(k => libButtonsForRelatedChildren[o._type][k](o)).join(' ')); + } + msgbox({ + title: title, + msg: html.pre(syntaxHighlight(o || '', replacer, msgobj).replace(/\n/g, '
')) + buttons, + sendas: sendas, + whisperto: whisperto, + headercss: headercss, + bodycss: bodycss + }); + return; + }; + const theme = { + primaryColor: '#222d3a', + primaryLightColor: '#ededed', + baseTextColor: '#232323', + secondaryColor: '#82b9b9' + }; + let localCSS = { + inlineEmphasis: { + 'font-weight': 'bold' + }, + hspacer: { + 'padding-top': '4px' + }, + textColor: { + 'color': theme.baseTextColor + }, + pre: { + 'border': `1px solid ${theme.baseTextColor}`, + 'border-radius': '5px', + 'padding': '4px 8px', + 'margin-top': '4px' + }, + msgbody: { + 'background-color': theme.primaryLightColor, + 'color': theme.baseTextColor + }, + msgheader: { + 'background-color': theme.primaryColor, + 'color': theme.primaryLightColor, + 'font-size': '1.2em' + }, + msgheadercontent: { + 'display': 'inline-block' + }, + msgheaderlogodiv: { + 'display': 'inline-block', + 'max-height': '30px', + 'margin-right': '8px', + 'margin-top': '4px' + }, + logoimg: { + 'background-color': 'transparent', + 'float': 'left', + 'border': 'none', + 'max-height': '30px' + }, + infoheader: { + 'background-color': theme.primaryColor, + 'color': theme.primaryLightColor, + 'font-size': '1.2em' + }, + infobody: { + 'background-color': theme.primaryLightColor, + 'color': theme.baseTextColor + }, + buttoncss: { + 'padding': '4px 8px', + 'background-color': theme.primaryColor, + 'color': theme.primaryLightColor, + 'border-radius': '5px', + 'line-height': '12px', + 'font-size': '12px' + }, + relatedLink: { + 'background-color': theme.primaryColor, + 'color': theme.primaryLightColor, + 'border-radius': '5px', + 'margin': '0px 4px', + 'line-height': '12px', + 'font-family': 'pictos', + 'font-size': '18px', + 'text-align': 'center', + 'width': '24px', + 'height': '12px', + 'vertical-align': 'middle' + }, + inlineLink: { + 'background-color': theme.secondaryColor, + 'color': theme.primaryLightColor, + 'padding': '1px 1px 2px 3px', + 'border-radius': '5px', + 'margin': '0px 1px 0px 3px', + 'line-height': '.95em', + 'font-family': 'pictos' + }, + tipContainer: { + 'overflow': 'hidden', + 'width': '100%', + 'border': 'none', + 'max-width': '250px', + 'display': 'block' + }, + tipBounding: { + 'border-radius': '10px', + 'border': '2px solid #000000', + 'display': 'table-cell', + 'width': '100%', + 'overflow': 'hidden', + 'font-size': '12px' + }, + tipHeaderLine: { + 'overflow': 'hidden', + 'display': 'table', + 'background-color': theme.primaryColor, + 'width': '100%' + }, + tipLogoSpan: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'vertical-align': 'middle', + 'width': '40px' + }, + tipLogoImg: { + 'min-height': '40px', + 'margin-left': '3px', + 'background-image': `url('${apilogoalt}')`, + 'background-repeat': 'no-repeat', + 'backgound-size': 'contain', + 'width': '37px', + 'display': 'inline-block' + }, + tipContentLine: { + 'overflow': 'hidden', + 'display': 'table', + 'background-color': theme.primaryLightColor, + 'width': '100%' + }, + tipContent: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'padding': '5px 8px', + 'text-align': 'left', + 'color': '#232323', + 'background-color': theme.primaryLightColor + }, + tipHeaderTitle: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'padding': '5px 8px', + 'text-align': 'left', + 'color': theme.primaryLightColor, + 'font-size': '1.2em', + 'vertical-align': 'middle', + 'font-weight': 'bold' + } + }; + const getTipFromObjForID = (obj) => { + let o = simpleObj(obj); + let contents = (validTypes[o._type] || validTypes.default)(o); + let tipHeader = contents.header || 'Info'; + let formattedContent = Object.keys(contents) + .filter(k => !['header', 'id'].includes(k.toLowerCase())) + .map(k => `• ${k}: ${contents[k]}`) + .join('
'); + return getTip(formattedContent, contents.ID, tipHeader); + }; + const getTipForColor = (color) => { + const localCSS = { + colorTip: { + 'width': '100%', + 'height': '50px', + 'min-height': '50px', + 'display': 'inline-block', + 'border': '0', + 'padding': '0', + 'margin': '0 auto', + 'vertical-align': 'middle', + 'background-color': color, + + } + }; + let content = html.span('', localCSS.colorTip); + return getTip(content, color, color); + }; + const getTip = (contents, label, header = 'Info', contentcss = {}) => { + let contentCSS = Object.assign(_.clone(localCSS.tipContent), contentcss); + return html.tip( + label, + html.span( // container + html.span( // bounding + html.span( // header line + html.span( // left (logo) + html.span('', localCSS.tipLogoImg), + localCSS.tipLogoSpan) + + html.span( // right (content) + header, + localCSS.tipHeaderTitle), + localCSS.tipHeaderLine) + + html.span( // content line + html.span( // content cell + contents, + contentCSS), + localCSS.tipContentLine), + localCSS.tipBounding), + localCSS.tipContainer), + { 'display': 'inline-block' } + ); + }; + // ================================================== + // UTILITIES + // ================================================== + 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 simpleObj = (o) => typeof o !== 'undefined' ? JSON.parse(JSON.stringify(o)) : o; + const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16))); + const escapePreserveLineBreaks = (s) => { + if (s && s.length) { + let aboutuuid = generateUUID(); + return HE(s.replace(/\n/g, aboutuuid)) + .replace(new RegExp(aboutuuid, 'g'), '
'); + } + return s; + }; + const validTypes = { + 'door': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Open: o.isOpen, + Locked: o.isLocked, + Secret: o.isSecret, + Position: `(${Math.round(o.path.handle0.x)}, ${Math.round(o.path.handle0.y)}), (${Math.round(o.path.handle1.x)}, ${Math.round(o.path.handle1.y)})`, + } + }, + 'window': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Open: o.isOpen, + Locked: o.isLocked, + Position: `(${Math.round(o.path.handle0.x)}, ${Math.round(o.path.handle0.y)}), (${Math.round(o.path.handle1.x)}, ${Math.round(o.path.handle1.y)})`, + } + }, + 'graphic': (o) => { + return { + header: (o._subtype || o._type).toUpperCase(), + ID: o._id, + Name: o.name, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Layer: o.layer, + Position: `(${Math.round(o.left)}, ${Math.round(o.top)})`, + Control: [...o.controlledby.split(/\s*,\s*/), + ...((getObj('character', o.represents) || { get: () => '' }).get('controlledby')).split(/\s*,\s*/)] + .map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', ') + }; + }, + 'character': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Journals: o.inplayerjournals.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + Control: o.controlledby.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', ') + }; + }, + 'attribute': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Character: (getObj('character', o._characterid) || { get: () => o._characterid || 'Unknown' }).get('name'), + Current: escapePreserveLineBreaks(o.current), + Max: escapePreserveLineBreaks(o.max) + } + }, + 'ability': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Character: (getObj('character', o._characterid) || { get: () => o._characterid || 'Unknown' }).get('name'), + Action: escapePreserveLineBreaks(o.action) + } + }, + 'macro': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Visible: o.visibleto.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + Creator: (getObj('player', o._playerid) || { get: () => o._playerid || 'Unknown' }).get('displayname'), + Action: escapePreserveLineBreaks(o.action) + } + }, + 'handout': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Journals: o.inplayerjournals.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + Control: o.controlledby.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + } + }, + 'rollabletable': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name + } + }, + 'tableitem': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Table: (getObj('rollabletable', o._rollabletableid) || { get: () => o._rollabletableid || 'Unknown' }).get('name'), + Weight: o.weight + } + }, + 'page': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Height: o.height, + Width: o.width + } + }, + 'deck': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name + } + }, + 'card': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + Deck: (getObj('deck', o._deckid) || { get: () => o._deckid || 'Unknown' }).get('name') + } + }, + 'hand': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Player: (getObj('player', o._parentid) || { get: () => o._parentid || 'Unknown' }).get('displayname'), + + } + }, + 'jukeboxtrack': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Title: o.title, + Volume: o.volume, + Loop: o.loop + } + }, + 'custfx': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name + } + }, + 'path': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Layer: o.layer, + Position: `(${Math.round(o.left)}, ${Math.round(o.top)})`, + Type: o.barrierType, + OneWay: o.oneWayReversed, + Control: o.controlledby.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + } + }, + 'pathv2': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Layer: o.layer, + Position: `(${Math.round(o.x)}, ${Math.round(o.y)})`, + Shape: o.shape, + Type: o.barrierType, + OneWay: o.oneWayReversed, + Control: o.controlledby.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + } + }, + 'text': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o._pageid) || { get: () => o._pageid || 'Unknown' }).get('name'), + Layer: o.layer, + Position: `(${Math.round(o.left)}, ${Math.round(o.top)})`, + Text: o.text || '', + Control: o.controlledby.split(/\s*,\s*/).map(p => p.toLowerCase() === 'all' ? 'All' : (getObj('player', p) || { get: () => p || 'Unknown' }).get('displayname')).join(', '), + } + }, + 'player': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + DisplayName: o._displayname, + GM: playerIsGM(o._id), + Page: getObj('page', getPageForPlayer(o)).get('name') + } + }, + 'campaign': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Page: (getObj('page', o.playerpageid) || { get: () => o.playerpageid || 'Unknown' }).get('name'), + Others: Object.keys(o.playerspecificpages).map(k => `${(getObj('player', k) || { get: () => k || 'Unknown' }).get('displayname')} (${(getObj('page', o.playerspecificpages[k]) || { get: () => o.o.playerspecificpages[k] || 'Unknown' }).get('name')})`) + } + }, + 'repeating': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + List: o.list, + RowID: o.rowid, + SubAttr: `
${o.subattr.join('
')}` + } + }, + 'list': (o) => { + return { + header: o._type.toUpperCase(), + ID: o._id, + Name: o.name, + SubAttr: `
${o.subattr.join('
')}` + } + }, + 'state key': (o) => { + return { + header: `STATE KEY`, + ID: o._id, + Name: o.name, + SubKeys: `
${o.subkeys.join('
')}` + } + }, + 'default': (o) => { + return { + header: `UNKOWN TYPE: ${o._type || 'missing'}`, + ID: o._id, + Props: Object.keys(o).join(', ') + } + } + }; + validTypes.fx = o => validTypes.custfx(o); + validTypes.token = o => validTypes.graphic(o); + validTypes.table = o => validTypes.rollabletable(o); + validTypes.track = o => validTypes.jukeboxtrack(o); + validTypes.item = o => validTypes.tableitem(o); + const validTypeTranslator = { + fx: 'custfx', + table: 'rollabletable', + track: 'jukeboxtrack', + item: 'tableitem' + }; + const getPageForPlayer = (p) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + if (playerIsGM(player.id)) { + return player.get('lastpage') || Campaign().get('playerpageid'); + } + + let psp = Campaign().get('playerspecificpages'); + if (psp[player.id]) { + return psp[player.id]; + } + + return Campaign().get('playerpageid'); + }; + const getCharactersForPlayer = (p, argObj) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + let limit = trueTypes.includes((argObj || { limit: false }).limit); + let testcases = [player.id]; + let characters = findObjs({ type: 'character' }); + if (!limit && playerIsGM(player.id)) { + return characters; + } + if (!limit) testcases.push('all'); + return characters.filter(c => { + return c.get('controlledby').split(',').filter(Set.prototype.has, new Set(testcases)).length; + }); + }; + const getTokensForPlayer = (p, argObj) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + let limit = trueTypes.includes((argObj || { limit: false }).limit); + let testcases = [player.id]; + let tokens = findObjs({ subtype: 'token' }); + if (!limit && playerIsGM(player.id)) { + return tokens; + } + if (!limit) testcases.push('all'); + return tokens.filter(t => { + return [...t.get('controlledby').split(','), + ...((getObj('character', t.get('represents')) || { get: () => '' }).get('controlledby')).split(',')] + .filter(Set.prototype.has, new Set(testcases)).length; + }); + }; + const getMacrosForPlayer = (p) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + return findObjs({ type: 'macro' }).filter(m => { + return [...m.get('visibleto').split(','), m.get('playerid')].includes(player.id); + }); + }; + const getHandoutsForPlayer = (p) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + return findObjs({ type: 'handout' }).filter(m => { + return [...m.get('inplayerjournals').split(','), + ...m.get('controlledby').split(',')].filter(Set.prototype.has, new Set([player.id, 'all'])); + }); + + }; + const getHandsForPlayer = (p) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + return findObjs({ type: 'hand', parentid: player.id }); + }; + const getCardsForPlayer = (p) => { + let player; + if (typeof p === 'string') player = getObj('player', p); + else { + if (p._id) player = getObj('player', p._id); + else if (p.id) player = getObj('player', p.id); + } + if (!player) return; + return findObjs({ type: 'graphic', subtype: 'card' }).filter(c => { + return [...c.get('controlledby').split(',')].filter(Set.prototype.has, new Set([player.id, 'all'])); + }); + }; + const getRepeatingForCharacter = (p, argObj) => { + if (!argObj.list) return; + let rpt = findObjs({ type: 'attribute' }) + .filter(c => c.get('characterid') === p._id) + .reduce((m, c) => { + let rptres = /^repeating_([^_]*?)_([^_]*?)_(.+)$/i.exec(c.get('name')); + if (!rptres || rptres[1].toLowerCase() !== argObj.list.toLowerCase()) return m; + let rowname = (m[rptres[2]] || { _id: '' })._id; + if (/name/i.test(rptres[3])) rowname = c.get('current'); + m[rptres[2]] = { + _id: rowname, + list: rptres[1], + rowid: rptres[2], + _type: 'repeating', + subattr: [...new Set([...(m[rptres[2]] || { subattr: [] }).subattr, rptres[3]])], + button: `!about --${rptres[2]}` + }; + return m; + }, {}); + return Object.keys(rpt).map(k => rpt[k]); + }; + const getListsForCharacter = (p) => { + return [...new Set( + findObjs({ type: 'attribute' }) + .filter(c => c.get('characterid') === p._id && /^repeating_([^_]*?)_([^_]*?)_(.+)$/i.test(c.get('name'))) + .map(c => /^repeating_([^_]*?)_([^_]*?)_(.+)$/i.exec(c.get('name'))[1])) + ].map(c => { + return { + name: c, + _id: c, + type: 'list', + _type: 'list', + subattr: [...new Set( + findObjs({ type: 'attribute' }) + .filter(a => new RegExp(`^repeating_${escapeRegExp(c)}_([^_]*?)_(.+)$`, 'i').test(a.get('name'))) + .map(a => new RegExp(`^repeating_${escapeRegExp(c)}_([^_]*?)_(.+)$`, 'i').exec(a.get('name'))[2]) + )], + button: `!about --typefor type=repeating for=${p.name} list=${c}` + } + }); + }; + const getAllRepeating = () => { + return findObjs({ type: 'attribute' }).filter(c => /^repeating_([^_]*?)_([^_]*?)_(.+)$/i.test(c.get('name'))); + }; + const getAllRepIDs = (r) => { + return [...new Set([...r.map(c => /^repeating_([^_]*?)_([^_]*?)_(.+)$/i.exec(c.get('name'))[2])])]; + }; + const libButtonsForRelatedChildren = { + page: { + player: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=player for=${p.name}`, label: html.tip('U', 'Players'), css: localCSS.relatedLink }), + token: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=token for=${p.name}`, label: html.tip('g', 'Tokens'), css: localCSS.relatedLink }), + graphic: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=graphic for=${p.name}`, label: html.tip('P', 'Graphics'), css: localCSS.relatedLink }), + path: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=path for=${p.name}`, label: html.tip('Y', 'Paths'), css: localCSS.relatedLink }), + text: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=text for=${p.name}`, label: html.tip('n', 'Text'), css: localCSS.relatedLink }), + door: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=door for=${p.name}`, label: html.tip('h', 'Doors'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Pictos Three' } } }), + window: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=window for=${p.name}`, label: html.tip('t', 'Windows'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Pictos Custom' } } }) + }, + rollabletable: { + tableitem: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=tableitem for=${p.name}`, label: html.tip('l', 'Items'), css: localCSS.relatedLink }) + }, + character: { + token: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=token for=${p.name}`, label: html.tip('g', 'Tokens'), css: localCSS.relatedLink }), + attribute: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=attribute for=${p.name}`, label: html.tip('@', 'Attributes'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Arial', 'font-size': '13px' } } }), + ability: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=ability for=${p.name}`, label: html.tip('%', 'Abilities'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Arial', 'font-size': '13px' } } }), + list: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=list for=${p.name}`, label: html.tip('l', 'Repeating Lists'), css: localCSS.relatedLink }), + }, + list: { + repeating: (p) => Messenger.Button({ type: '!', elem: p.button, label: html.tip('l', 'List Items'), css: localCSS.relatedLink }) + }, + player: { + mycharacters: (p) => Messenger.Button({ type: '!', elem: `!about --typefor limit=true type=character for=${p._displayname}`, label: html.tip('U', 'My Characters'), css: { ...localCSS.relatedLink, ...{ 'color': theme.secondaryColor } } }), + character: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=character for=${p._displayname}`, label: html.tip('U', 'Characters'), css: localCSS.relatedLink }), + mytokens: (p) => Messenger.Button({ type: '!', elem: `!about --typefor limit=true type=token for=${p._displayname}`, label: html.tip('g', 'My Tokens'), css: { ...localCSS.relatedLink, ...{ 'color': theme.secondaryColor } } }), + token: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=token for=${p._displayname}`, label: html.tip('g', 'Tokens'), css: localCSS.relatedLink }), + macro: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=macro for=${p._displayname}`, label: html.tip('e', 'Macros'), css: localCSS.relatedLink }), + handout: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=handout for=${p._displayname}`, label: html.tip('N', 'Handouts'), css: localCSS.relatedLink }), + hand: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=hand for=${p._displayname}`, label: html.tip('|', 'Hands'), css: localCSS.relatedLink }), + card: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=card for=${p._displayname}`, label: html.tip('k', 'Cards'), css: localCSS.relatedLink }), + } + } + const libRelatedChildren = { // p will be a simpleObj + page: { + token: (p) => findObjs({ subtype: 'token' }).filter(c => c.get('pageid') === p._id), + graphic: (p) => findObjs({ type: 'graphic' }).filter(c => c.get('pageid') === p._id), + path: (p) => findObjs({ type: 'path' }).filter(c => c.get('pageid') === p._id), + text: (p) => findObjs({ type: 'text' }).filter(c => c.get('pageid') === p._id), + player: (p) => findObjs({ type: 'player' }).filter(c => getPageForPlayer(c) === p._id), + door: (p) => findObjs({ type: 'door' }).filter(c => c.get('pageid') === p._id), + window: (p) => findObjs({ type: 'window' }).filter(c => c.get('pageid') === p._id) + }, + rollabletable: { + tableitem: (p) => findObjs({ type: 'tableitem' }).filter(c => c.get('rollabletableid') === p._id) + }, + character: { + attribute: (p) => findObjs({ type: 'attribute' }).filter(c => c.get('characterid') === p._id), + ability: (p) => findObjs({ type: 'ability' }).filter(c => c.get('characterid') === p._id), + token: (p) => findObjs({ subtype: 'token' }).filter(c => c.get('represents') === p._id), + list: getListsForCharacter, + }, + list: { + repeating: getRepeatingForCharacter + }, + player: { + character: getCharactersForPlayer, + token: getTokensForPlayer, + macro: getMacrosForPlayer, + handout: getHandoutsForPlayer, + hand: getHandsForPlayer, + card: getCardsForPlayer, + } + }; + const getParentsForChildrenOfType = t => { + return Object.keys(libRelatedChildren).filter(pk => libRelatedChildren[pk].hasOwnProperty(t)); + }; + const reduceByType = (ret) => { + if (!ret) return; + ret.bytype = ret.bytype || {}; + ret.obj = ret.obj.map(o => { + o = simpleObj(o); + let type = o._type; + if (type === 'player' && playerIsGM(o._id)) type += ' (gm)'; + // ret.bytype[o._type] = [...(ret.bytype[o._type] || []), o]; + ret.bytype[type] = [...(ret.bytype[type] || []), o]; + return o; + }); + return ret; + }; + const getUnknown = (query, msg, onlyfirst = true) => { + let ret = lexicalGet(query, msg); + ret = ret || fuzzyGet(query, msg, onlyfirst); + return ret || { fail: true, reason: 'notfound' }; + } + const lexicalGet = (query, msg) => { + let ret; + let res; + const types = Object.keys(validTypes); + const canIds = playerIsGM(msg.playerid) || manageState.get('playersCanIDs'); + let optionrx = /([^\s=/]+)=((?:=(?<=\/=)|[^=])*?)(?=(?:[^\s=/]+=.*|$))/g; + + if (/state(\.|$)/i.test(query)) { + if (!canIds) return { fail: true, reason: 'canids' }; + if (/state$/i.test(query)) { + ret = reduceByType({ + name: query, + obj: Object.keys(state) + .map(k => { + return { + name: k, + _id: `${k}`, + _type: 'state key', + subkeys: Object.keys(state[k]).map(sk => `${sk} (${typeof state[k][sk]})`), + button: `!about --state.${k}` + } + }) + }); + } else { + res = [query.split('.').slice(1) + .reduce((m, k) => { + if (m) m = m[k]; + return m; + }, state)]; + if (res[0]) ret = { name: query, obj: res }; + else ret = { fail: true, reason: 'notfound' }; + } + } else if (/^(msg|message)/i.test(query)) { + ret = { name: 'Message', obj: [msg] }; + } else if (/^(inline|inlinerolls?|rolls)/i.test(query)) { + ret = msg.inlinerolls && msg.inlinerolls.length ? { name: 'Rolls', obj: [msg.inlinerolls] } : { fail: true, reason: 'msgpart' }; + } else if (/^selected/i.test(query)) { + ret = msg.selected && msg.selected.length ? { name: 'Selected', obj: [msg.selected] } : { fail: true, reason: 'msgpart' }; + } else if (/^\$\[\[(\d+)]]/.test(query)) { + res = /^\$\[\[(\d+)]]/.exec(query); + ret = msg.inlinerolls && msg.inlinerolls.length > res[1] ? msg.inlinerolls[res[1]] : undefined; + if (ret) ret = { name: `Roll ${res[1]} (${msg.inlinerolls[res[1]].expression})`, obj: [ret] }; + } else if (/^type\s+([^\s]+.*)/i.test(query)) { + res = /^type\s+([^\s]+.*)/i.exec(query)[1] + .split(/\s+/) + .map(t => t.toLowerCase()) + .filter(t => types.includes(t)) + .map(t => [...findObjs({ type: validTypeTranslator[t] || t }), ...findObjs({ subtype: validTypeTranslator[t] || t })]) + .reduce((m, t) => [...m, ...t], []); + if (res.length) { + ret = reduceByType({ name: 'By Type', obj: res }); + } + } else if (/^typefor\s([^\s]+.*)/i.test(query)) { + let parent; + let children; + let parval; + let childval; + let potentials; + let potparenttypes; + let settype; + let argObj = {}; + /^typefor\s([^\s]+.*)/i.exec(query)[1].replace(optionrx, (m, prop, val) => { + switch (prop.toLowerCase()) { + case 'type': + childval = val.trim(); + break; + case 'for': + parval = val.trim(); + break; + default: + argObj[prop.toLowerCase()] = val.trim(); + } + }); + if (!(parval && childval)) return { fail: true, reason: 'notfound' }; + // childval = types.filter(t => t === childval.toLowerCase())[0]; + childval = validTypeTranslator[childval.toLowerCase()] || childval.toLowerCase(); + potentials = fuzzyGet(parval, msg, false); + if (!potentials || !potentials.obj.length) return { fail: true, reason: 'notfound' }; + if (potentials.bytype.hasOwnProperty('player (gm)')) potentials.bytype.player = [...(potentials.bytype.player || []), ...potentials.bytype['player (gm)']]; + if (potentials.obj.length === 1) { + parent = potentials.obj[0]; + settype = childval === 'repeating' && argObj.list ? 'list' : parent._type; + } else { + potparenttypes = getParentsForChildrenOfType(childval); + settype = childval === 'repeating' && argObj.list && potentials.bytype.hasOwnProperty('character') ? 'list' : Object.keys(potentials.bytype).filter(k => potparenttypes.includes(k))[0]; + if (settype) { + if (settype === 'list') parent = potentials.bytype.character[0]; + else parent = potentials.bytype[settype][0]; + } + } + if (parent && libRelatedChildren[settype] && libRelatedChildren[settype][childval] && typeof libRelatedChildren[settype][childval] === 'function') { + children = libRelatedChildren[settype][childval](parent, argObj); + ret = reduceByType({ name: `${settype === 'list' ? argObj.list.toUpperCase() : childval.toUpperCase()}(S) FOR ${parval.toUpperCase()}`, obj: children }); + } else ret = { fail: true, reason: 'notfound' }; + } else { + if (!msg.allRepeating) msg.allRepeating = getAllRepeating(); + if (getAllRepIDs(msg.allRepeating).includes(query)) { + let queryrx = new RegExp(`repeating_([^_]*?)_${query}_(.+)$`, 'i'); + let children = msg.allRepeating.filter(c => queryrx.test(c.get('name'))); + let parent = simpleObj(getObj('character', children[0].get('characterid'))); + let thelist = queryrx.exec(children[0].get('name'))[1]; + ret = reduceByType({ name: `${thelist.toUpperCase()} ENTRY FOR ${parent.name.toUpperCase()}`, obj: children }); + } + } + return ret; + }; + + const fuzzyGet = (query, msg, onlyfirst = true) => { + let ret; + let res; + const validProps = ['name', 'title', 'text', 'displayname']; + const canIds = playerIsGM(msg.playerid) || manageState.get('playersCanIDs'); + if (canIds) validProps.unshift('id'); + while (validProps.length) { + if (onlyfirst && ret) break; + const prop = validProps.shift(); + res = findObjs({ [prop]: query }); + if (res.length) { + if (!ret) ret = { name: query, obj: res }; + else ret.obj = [...ret.obj, ...res]; + } + } + ret = reduceByType(ret); + return ret; + }; + const failHandler = (wto, altmsg = 'default') => { + const messages = { + canids: 'You must be a GM or have your GM enable the playerCanIds setting for Inspector to use this feature.', + notfound: 'Unable to find an object using the parameters supplied. Please try again.', + default: 'You must be a GM or have your GM enable the playersCanUse setting for Inspector to use this feature.', + msgpart: 'Message does not contain that component. Please try again.' + }; + messages.notfoundid = `${messages.notfound} If you were searching by ID, it is possible that the object exists, but Inspector is not currently configured to allow players to use IDs. Your GM can enable this feature, if needed.` + altmsg = Object.keys(messages).map(k => k.toLowerCase()).includes(altmsg.toLowerCase()) ? altmsg.toLowerCase() : 'default'; + msgbox({ msg: messages[altmsg], title: 'Inspection Failed', whisperto: wto }); + }; + const msgbox = ({ + msg: msg = '', + title: title = '', + headercss: headercss = localCSS.msgheader, + bodycss: bodycss = localCSS.msgbody, + sendas: sendas = 'Inspector', + whisperto: whisperto = '', + footer: footer = '', + btn: btn = '', + } = {}) => { + if (title) title = html.div(html.div(html.img(apilogoalt, 'Inspector 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, noarchive: true }); + }; + const helpPanel = (wto) => { + msgbox({ + title: 'Inspector Help', + msg: html.h2(`Help`, localCSS.textColor) + + `${html.span(`Inspector`, localCSS.inlineEmphasis)} is designed to help you easily look at objects in your game and view their properties. ` + + `Search by id, name, type, or other specialized parameters. All objects answering to that identifying piece of information will be reported, allowing you to examine them more closely. ` + + `Roll20 object IDs are detected in the output and turned into links so that you can navigate from one object to a related object easily. ` + + `Here are the particulars of the script's use. ` + + html.h3('Command Line', localCSS.textColor, localCSS.hspacer) + + `Use ${html.span('!about', localCSS.inlineEmphasis)} followed by arguments of the things you want to inspect. Arguments should be set off with double hyphens:` + + html.pre('!about --Kraang the Conciliatory', localCSS.pre) + + `Multiple arguments can be included. Each will produce a panel of returns.` + + html.pre('!about --Fire Ball --Kraang the Really Quite Agreeable', localCSS.pre) + + html.h3('Returns', localCSS.textColor, localCSS.hspacer) + + `If your argument returns a single thing, you will see a detailed breakdown of the way that object is structured in your game, allowing you to pinpoint a particular property name or check ` + + `the value as it is stored in the object. Certain datapoints (Roll20 IDs, recognizable hex color strings, and token marker names) are formatted to have a hover tip providing you more ` + + `information very quickly. Also, the Roll20 IDs that are present in the output are also paired with a link to let you pull up that object in Inspector for a for detailed examination.

` + + `If, on the other hand, you get a number of returns for your search criteria, they will be presented by category of the object. For instance, the command line:` + + html.pre('!about --Kraang the No Idea Is a Bad Idea Leader', localCSS.pre) + + `Might produce a return for a character going by that name, as well as all tokens representing this super-progressive character (and thus sharing a name).` + + html.h4('Extended Returns', localCSS.textColor, localCSS.hspacer) + + `Some items are related to each other in the game even though they might not show up on the initial property panel as directly attached as a javascript property. You will see these returns ` + + `represented in the returns panel as buttons at the bottom. They include such relationships as attributes, abilities, repeating lists, or tokens for a character, characters for a player, tokens ` + + `for a page, etc. Each of the buttons has a hover-tip to tell you what it represents, if the chosen icon is not clear enough. (See ${html.span('TextFor', localCSS.inlineEmphasis)}, for more information)` + + html.h4('Return Types', localCSS.textColor, localCSS.hspacer) + + `For the most part, the returned types represent object types in a Roll20 game. In an effort to present more information, Inspector deviates from this in one or two places. ` + + `First, there is no discrete Roll20 object for a repeating ${html.span('list', localCSS.inlineEmphasis)}, nor for ${html.span('repeating', localCSS.inlineEmphasis)} as an object type separate from ` + + `an ${html.span('attribute', localCSS.inlineEmphasis)}, nor is there a foreign-key-style relationship between a list and an entry on that list, nor between a list entry and the various sub-attributes ` + + `that are a part of that entry (the relationship is a bit more complex than that). Similarly, there is no object-level distinction between a ${html.span('player', localCSS.inlineEmphasis)} object ` + + `who is a GM versus one who is not. All of this data can be determined, however, and Inspector is built to allow you to flow between these related objects.` + + html.h3('Argument Types', localCSS.textColor, localCSS.hspacer) + + `You have a few options for what to use in an argument. And since every argument produces a different panel of returns, they need not be related.` + + html.h4('General Text', localCSS.textColor, localCSS.hspacer) + + `Text not recognized as one of the special arguments below will be used as search criteria across all objects in your game. Inspector will look for matches in the ` + + `${html.span('id', localCSS.inlineEmphasis)}, ${html.span('name', localCSS.inlineEmphasis)}, ${html.span('displayname', localCSS.inlineEmphasis)} (for players), ${html.span('title', localCSS.inlineEmphasis)} (for jukebox tracks), ` + + `or ${html.span('text', localCSS.inlineEmphasis)} (for text objects) properties. (At this point, your supplied criteria must match fully what is in the property. Perhaps at some point ` + + `in the future Inspector will be able to perform partial matches.)` + + html.h4('Message', localCSS.textColor, localCSS.hspacer) + + `Use ${html.span('message', localCSS.inlineEmphasis)} or ${html.span('msg', localCSS.inlineEmphasis)} to look at this message.` + + html.pre('!about --message', localCSS.pre) + + `This is helpful if you want to see the way a message comes structured from Roll20, including any rolls or selected tokens. Remember, messages are handed off from script ` + + `to script, so by the time Inspector sees the message it may have been altered by other scripts (especially metascripts).` + + html.h4('Selected', localCSS.textColor, localCSS.hspacer) + + `Use ${html.span('selected', localCSS.inlineEmphasis)} to specifically see the data in the message object for any selected tokens.` + + html.pre('!about --selected', localCSS.pre) + + html.h4('Rolls', localCSS.textColor, localCSS.hspacer) + + `Use any of ${html.span('rolls', localCSS.inlineEmphasis)}, ${html.span('inline', localCSS.inlineEmphasis)}, or ${html.span('inlinerolls', localCSS.inlineEmphasis)} to ` + + `view the inline rolls that are a part of the message. The rolls can be included almost anywhere in the command line, from just after the script handle to after the ${html.span('rolls', localCSS.inlineEmphasis)} handle:` + + html.pre(`!about ${HE('[[2d20kh1]]')} --rolls
!about --rolls ${HE('[[ 1d[[2d20kl1]] ]]')}`, localCSS.pre) + + `It also works to put an inline roll as the argument, itself, to see that roll expanded in its own panel:` + + html.pre(`!about --${HE('[[2d20kl1]]')}`, localCSS.pre) + + html.h4('State', localCSS.textColor, localCSS.hspacer) + + `A game's state is where data that requires tracking between sessions or sandbox reboots is stored. It is the most permanent storage available to a script, so scripters often use it for user preferences, script configurations, or caching. ` + + `You can get a look at the state (or component parts of it) by using the word ${html.span('state', localCSS.inlineEmphasis)}.` + + html.pre(`!about --state
`, localCSS.pre) + + `Depending on the number of scripts you have installed and how much the developers responsible for those scripts have utilized the state object, you might have quite a sizable return. In that case, ` + + `you might wish to see a smaller section of the state. You can use dot notation to drill down to properties attached to the state object, narrowing the scope of your returns:` + + html.pre(`!about --state.Inspector
!about --state.Inspector.settings`, localCSS.pre) + + html.h4('Type', localCSS.textColor, localCSS.hspacer) + + `The keyword ${html.span('type', localCSS.inlineEmphasis)} gives you the opportunity to return all things associated with one or more Roll20 object types. Include the types ` + + `you want to search for after a space, and separate each with a space:` + + html.pre(`!about --type player
!about --type character token`, localCSS.pre) + + `For these searches, you will very likely have more than one return, so you will see the panel of categorized results showing the objects Inspector found. ` + + `Use the ${html.span('View', localCSS.inlineEmphasis)} button to view a more detailed breakdown of an individual object.` + + `The following types are recognized:` + + html.pre(`ability
attribute
campaign
card
character
custfx (also: fx)
deck
graphic
hand
handout
jukeboxtrack (also: track)
list
` + + `macro
page
path
player
repeating
rollabletable (also: table)
tableitem (also: item)
text
token`, localCSS.pre) + + html.h4('TypeFor', localCSS.textColor, localCSS.hspacer) + + `The ${html.span('typefor', localCSS.inlineEmphasis)} keyword lets you build object lists from objects that are related by game context, if not directly by property attachment. ` + + `This could include tokens on a page, or characters for a player. The full set of ${html.span('typefor', localCSS.inlineEmphasis)} combinations is given, below. To use them, begin the ` + + `argument with ${html.span('typefor', localCSS.inlineEmphasis)}, followed by the sub-parts ${html.span('for', localCSS.inlineEmphasis)} and ${html.span('type', localCSS.inlineEmphasis)} ` + + `set equal to the appropriate value: ` + + html.pre(`!about --typefor type=player for=Start
!about --typefor type=token for=Kraang Gifter of Office Mints`, localCSS.pre) + + `In the first one, you would be asking for players currently on the Start page. The second example asks for tokens associated with the ever-more-benevolent Kraang.

` + + `For certain combinations (such as entries on a repeating list), a third argument, ${html.span('list', localCSS.inlineEmphasis)}, is required:` + + html.pre(`!about --typefor type=repeating list=traits for=Kraang Bringer of Bagels`, localCSS.pre) + + `It does not matter in which order the sub-arguments come. The ${html.span('type', localCSS.inlineEmphasis)} sub-argument should be singular, and the ${html.span('for', localCSS.inlineEmphasis)} ` + + `sub-argument should be a way to identify a parent object.` + + html.h5(`TypeFor Combinations`, localCSS.textColor, localCSS.hspacer) + + `The following combinations will work in the ${html.span('typefor', localCSS.inlineEmphasis)} argument:` + + html.pre(`For a character:
-- attribute
-- ability
-- token
-- list
` + + `For a page:
-- token
-- graphic
-- path
-- text
-- player
` + + `For a player:
-- character
-- token
-- macro
-- handout
-- hand
-- card
` + + `For a table:
-- tableitem
` + + `For a list:
-- repeating (requires list sub-argument)`, localCSS.pre) + + html.h3('A Note About Hover Tips', localCSS.textColor, localCSS.hspacer) + + `As mentioned, Roll20 IDs, hex color strings, and token markers are hoverable items. If the tip is attached to an object's ID, it will contain the most relevant information for that object. Colors ` + + `and markers will show a preview. Token markers are only detected in the detailed look at a token (in the ${html.span('statusmarkers', localCSS.inlineEmphasis)} property), and in the Campaign detail ` + + `in the ${html.span('_token_markers', localCSS.inlineEmphasis)} property.` + + html.h2('Configuration', localCSS.textColor, localCSS.hspacer) + + `Use the script handle ${html.span('aboutconfig', localCSS.inlineEmphasis)} to change script settings. As of this release, script arguments are not case-sensitive.` + + html.h3('Booleans', localCSS.textColor, localCSS.hspacer) + + `Boolean properties are set to ${html.span('true', localCSS.inlineEmphasis)} if they are set to any of the following: ${html.span('true', localCSS.inlineEmphasis)}, ${html.span('t', localCSS.inlineEmphasis)}, ` + + `${html.span('yes', localCSS.inlineEmphasis)}, ${html.span('yep', localCSS.inlineEmphasis)}, ${html.span('yup', localCSS.inlineEmphasis)}, ${html.span('y', localCSS.inlineEmphasis)}, ` + + `${html.span('+', localCSS.inlineEmphasis)}, or ${html.span('keith', localCSS.inlineEmphasis)}. Any other value passed will evaluate as ${html.span('false', localCSS.inlineEmphasis)}.` + + html.h4('playersCanUse', localCSS.textColor, localCSS.hspacer) + + `Because Inspector offers a way to glimpse game or campaign data not otherwise easily viewable, it comes pre-configured to only allow GMs to use it. You can allow players to use the script by setting ` + + `${html.span('playersCanUse', localCSS.inlineEmphasis)} to true:` + + html.pre('!aboutconfig --playerscanuse=keith', localCSS.pre) + + html.h4('playersCanIDs', localCSS.textColor, localCSS.hspacer) + + `If the players can use the script, can they search by ID? Honestly, this one is less useful given the amount of information that is presented even in hover tips, so I would suggest ` + + `relying on the ${html.span('playersCanUse', localCSS.inlineEmphasis)} setting more than this one. However, if you find a case where you would like your players able to search only by name/text/title, ` + + `you can control access with the ${html.span('playersCanIDs', localCSS.inlineEmphasis)} setting:` + + html.pre('!aboutconfig --playerscanids=false', localCSS.pre) + + html.h2(`About`, localCSS.textColor, localCSS.hspacer) + + `${html.span(`version: ${version}`, localCSS.inlineEmphasis)}
This bit of scriptometry brought to you by ${html.a('timmaugh, the Metamancer', 'https://app.roll20.net/users/5962076/timmaugh')}.` + , + wto: wto + }); + }; + // ================================================== + // HANDLE INPUT + // ================================================== + const apihandles = { + about: /^!(?:about|inspect)\b/i, + aboutfirst: /^!(?:about|inspect)first\b/i, + aboutconfig: /^!(?:about|inspect)config\b/i + }; + const testConstructs = (c) => { + return Object.keys(apihandles).reduce((m, k) => { + if (!m.length) m = m || apihandles[k].test(c) ? k : ''; + apihandles[k].lastIndex = 0; + return m; + }, ''); + }; + const handleInput = (msg) => { + if (!msg.type === 'api' || !testConstructs(msg.content).length) return; + let wto = /^![^\s\(]+\(chat\)/.test(msg.content) ? '' : msg.who.replace(/\s\(gm\)$/i, ''); + if (!(playerIsGM(msg.playerid) || manageState.get('playersCanUse'))) { + failHandler(wto); + return; + } + let args = msg.content.split(/\s+--/g); + let o; + let table = '', rows = ''; + msg.aboutUUID = `About${generateUUID()}`; + switch (testConstructs(msg.content)) { + case 'aboutfirst': + args.slice(1).forEach(a => { + o = getUnknown(a, msg); + if (!o || o.fail) failHandler(wto, (o || { reason: `notfound${!manageState.get('playersCanIDs') && !playerIsGM(msg.playerid) ? 'id' : ''}` }).reason); + else if (o) showObjInfo({ o: o.obj[0], title: o.name, whisperto: wto, headercss: localCSS.infoheader, bodycss: localCSS.infobody, msgobj: msg }); + else msgbox({ msg: `No object found for ${a}.${!manageState.get('playersCanIDs') && !playerIsGM(msg.playerid) ? ' If you were searching by ID, it is possible that the object exists, but Inspector is not currently configured to allow players to use IDs. Your GM can enable this feature, if needed.' : ''}`, title: `No Object Found`, whisperto: wto }); + }); + break; + case 'about': + if (args.length === 1) { // no arguments means help panel + helpPanel(wto); + } else { + args.slice(1).forEach(a => { + o = getUnknown(a, msg, false) + if (!o || o.fail) failHandler(wto, (o || { reason: `notfound${!manageState.get('playersCanIDs') && !playerIsGM(msg.playerid) ? 'id' : ''}` }).reason); + else if (o && o.obj && o.obj.length === 1) showObjInfo({ o: o.obj[0], title: o.name, whisperto: wto, headercss: localCSS.infoheader, bodycss: localCSS.infobody, msgobj: msg }); + else { + rows = Object.keys(o.bytype).map(k => { + return html.tr(html.td(k.toUpperCase()) + html.td('', { width: '50px' }), { 'border-bottom': '1px solid #222d3a', 'font-size': '14px', 'font-weight': 'bold' }) + o.bytype[k].map(item => { + return `${html.tr(html.td(getTipFromObjForID(item)) + html.td(Messenger.Button({ type: '!', elem: item.button || '!about --' + item._id, label: 'View', css: localCSS.buttoncss }), { width: '50px' }))}`; + }).join(''); + }).join(''); + table = html.table(rows, { width: '100%' }); + msgbox({ msg: table, title: o.name, whisperto: wto }); + } + }); + } + break; + case 'aboutconfig': + if (!playerIsGM(msg.playerid)) { + failHandler(wto, 'canids'); + return; + } + o = {}; + args.slice(1).forEach(a => { + if (!/([^\s=/]+)\s*=\s*(.*)/.test(a)) return; + let [_m, prop, val] = /([^\s=/]+)\s*=\s*(.*)/.exec(a); // eslint-disable-line no-unused-vars + let sanisetting = propSanitation(prop, val); + if (sanisetting) { + o[sanisetting.prop] = sanisetting.val; + manageState.set(sanisetting.prop, sanisetting.val); + } + }); + if (Object.keys(o).length) { + msgbox({ + title: 'Settings Changed', + whisperto: wto, + msg: `You made the following changes to Inspector:
${Object.keys(o).map(k => `• ${k} : ${o[k]}`).join('
')}` + }); + } + break; + default: + return; + } + + }; + + const registerEventHandlers = () => { + on('chat:message', handleInput); + + }; + + 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: {} }; + deps.forEach(d => { + if (!d.mod) { + result.passed = false; + result.failures[d.name] = `Not found.`; + return; + } + if (d.version && d.version.length) { + //let [prop, version] = ['version', ...d.version.split('::')].slice(-2); + if (!(API_Meta[d.name].version && API_Meta[d.name].version.length && versionCheck(API_Meta[d.name].version, d.version))) { + result.passed = false; + result.failures[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) { + m.passed = false; + m.failures[d.name] = `Incorrect version.`; + } + return m; + }, result); + }); + return result; + }; + let depCheck = dependencyEngine(deps); + if (!depCheck.passed) { + let failures = Object.keys(depCheck.failures).map(k => `• ${k} : ${depCheck.failures[k]}`).join('
'); + let contents = `${apiproject} requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:
${failures}`; + let msg = `
MISSING MOD DETECTED
${contents}
`; + sendChat(apiproject, `/w gm ${msg}`); + return false; + } + return true; + }; + + on('ready', () => { + versionInfo(); + assureState(); + logsig(); + let reqs = [ + { + name: 'Messenger', + version: `1.0.0.b3`, + mod: typeof Messenger !== 'undefined' ? Messenger : undefined, + checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function']] + }, + { + name: 'libTokenMarkers', + version: `0.1.2`, + mod: typeof libTokenMarkers !== 'undefined' ? libTokenMarkers : undefined, + checks: [['getStatus', 'function'], ['getStatuses', 'function'], ['getOrderedList', 'function']] + } + ]; + if (!checkDependencies(reqs)) return; + html = Messenger.Html(); + css = Messenger.Css(); + HE = Messenger.HE; + registerEventHandlers(); + }); + return { + version: version + }; +})(); + +{ try { throw new Error(''); } catch (e) { API_Meta.Inspector.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Inspector.offset); } } +/* */ \ No newline at end of file diff --git a/Inspector/Inspector.js b/Inspector/Inspector.js index 5bb72fa64..5484da84d 100644 --- a/Inspector/Inspector.js +++ b/Inspector/Inspector.js @@ -3,8 +3,8 @@ Name : Inspector GitHub : Roll20 Contact : timmaugh -Version : 1.0.3 -Last Update : 19 JULY 2025 +Version : 1.0.4 +Last Update : 27 DEC 2025 ========================================================= */ var API_Meta = API_Meta || {}; @@ -17,10 +17,10 @@ const Inspector = (() => { // eslint-disable-line no-unused-vars const apiproject = 'Inspector'; const apilogo = `https://i.imgur.com/N9swrPX.png`; // black for light backgrounds const apilogoalt = `https://i.imgur.com/xFOQhK5.png`; // white for dark backgrounds - const version = '1.0.3'; + const version = '1.0.4'; const schemaVersion = 0.1; API_Meta[apiproject].version = version; - const vd = new Date(1752969000545); + const vd = new Date(1766852906054); 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}`); }; @@ -852,7 +852,7 @@ const Inspector = (() => { // eslint-disable-line no-unused-vars graphic: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=graphic for=${p.name}`, label: html.tip('P', 'Graphics'), css: localCSS.relatedLink }), path: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=path for=${p.name}`, label: html.tip('Y', 'Paths'), css: localCSS.relatedLink }), text: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=text for=${p.name}`, label: html.tip('n', 'Text'), css: localCSS.relatedLink }), - door: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=door for=${p.name}`, label: html.tip('h', 'Doors'), css: { ...localCSS.relatedLink, ...{'font-family': 'Pictos Three'} } }), + door: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=door for=${p.name}`, label: html.tip('h', 'Doors'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Pictos Three' } } }), window: (p) => Messenger.Button({ type: '!', elem: `!about --typefor type=window for=${p.name}`, label: html.tip('t', 'Windows'), css: { ...localCSS.relatedLink, ...{ 'font-family': 'Pictos Custom' } } }) }, rollabletable: { @@ -1206,7 +1206,7 @@ const Inspector = (() => { // eslint-disable-line no-unused-vars }; const handleInput = (msg) => { if (!msg.type === 'api' || !testConstructs(msg.content).length) return; - let wto = msg.who.replace(/\s\(gm\)$/i, ''); + let wto = /^![^\s\(]+\(chat\)/.test(msg.content) ? '' : msg.who.replace(/\s\(gm\)$/i, ''); if (!(playerIsGM(msg.playerid) || manageState.get('playersCanUse'))) { failHandler(wto); return; diff --git a/Inspector/script.json b/Inspector/script.json index f45d7fbd9..5a0e29d7a 100644 --- a/Inspector/script.json +++ b/Inspector/script.json @@ -1,7 +1,7 @@ { "name": "Inspector", "script": "Inspector.js", - "version": "1.0.3", + "version": "1.0.4", "description": "Inspector is a tool for those curious about data in their Roll20 game, especially those who might be thinking about starting down the scripting road. Use simple command line switches to produce panels of information in formatted-JSON, complete with hover-tips, links, and browsable extended returns.\r\rInspector is designed to help you easily look at objects in your game and view their properties. Search by id, name, type, or other specialized parameters. All objects answering to that identifying piece of information will be reported, allowing you to examine them more closely. Roll20 object IDs are detected in the output and turned into links so that you can navigate from one object to a related object easily. Run !about to get an in-game help panel.\r\r[Original Forum Thread](https://app.roll20.net/forum/permalink/11160380/)", "authors": "timmaugh", "roll20userid": "5962076", @@ -12,6 +12,7 @@ "previousversions": [ "1.0.0", "1.0.1", - "1.0.2" + "1.0.2", + "1.0.3" ] } \ No newline at end of file diff --git a/SelectManager/1.1.13/SelectManager.js b/SelectManager/1.1.13/SelectManager.js new file mode 100644 index 000000000..536106535 --- /dev/null +++ b/SelectManager/1.1.13/SelectManager.js @@ -0,0 +1,1161 @@ +/* +========================================================= +Name : SelectManager +GitHub : https://github.com/TimRohr22/Cauldron/tree/master/SelectManager +Roll20 Contact : timmaugh && The Aaron +Version : 1.1.13 +Last Update : 27 DEC 2025 +========================================================= +*/ +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.13'; + 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(1766852681848); + 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 = /^\/(?.*?)\/(?(?: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 asGM = (msg) => playerIsGM(msg.playerid) || msg.hasOwnProperty('reactionProps'); + const getTokens = (query, msg, owner = true) => { + if (msg.playerid === 'API') msg.playerid = preservedMsgObj[maintrigger].playerid; + let pageid = getPageForPlayer(msg.playerid); + 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' || asGM(msg)); + if (owner) { + alltokens = alltokens.filter(t => asGM(msg) || playersCanUseIDs() || playerCanControl(t, msg.playerid)); + } + 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('
'); + msgbox({ title: `SelectManager Config Changed`, msg: `You have made the following changes to the SelectManager configuration:
${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('
'); + message = `SelectManager is currently configured as follows:
${cfgTrack.playerscanids}
${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 => `
  • ${a.get('name')} (${a.get('type') === 'ability' ? `ability for ${getObj('character', a.get('characterid')).get('name')}` : 'macro'})
  • `).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:

    +noble`) + + 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 +npc 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 (*) immediately following the "+" (for "should have") or "-" ` + + `(for "should not have"):

    +*noble
    +*noble > 2`) + + 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:` + + `
      ${listAffected}
    `); + //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('
    '); + 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 = /(?.+?)(?:\?(?\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 ''; } }) => { + let control = ( + obj.get('represents') && obj.get('represents').length + ? getObj('character', obj.get('represents') || { get: function () { return ''; } }) + : obj + ) + .get('controlledby').split(/,/); + if (!control.length) return true; + return !control.filter(s => s.length && !playerIsGM(s)).length; + }; + const isParty = (obj = { get: () => { return ''; } }) => { + let char = ( + obj.get('represents') && obj.get('represents').length + ? getObj('character', obj.get('represents') || { get: function () { return ''; } }) + : obj + ); + return char.get('inParty'); + }; + 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', '4'].includes(c.ident) ? c.ident : '1'}_value`), c.value]; + } + break; + case 'max': + if (typeProcessor.hasOwnProperty(test)) { + comp = [t.get(`bar${['1', '2', '3', '4'].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 = /^(?[^.|#?]+?)(?:(?:\.|\?|#|\|)(?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; + case 'party': + if (t.get('type') === 'graphic' && t.get('subtype') === 'token' && t.get('layer') === 'objects') { + test = '='; + comp = [isParty(t), true]; + } + break; + + 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 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 = /^(?\+|-)(?@|\*|#)?(?[^\s><=!~]+)(?:\s*$|\s*(?>=|<=|~|!~|=|!=|<|>|in(?=\s+\[[^\]]+\]))\s*(?.+)$)/; + const typeitemrx = /^(?bar|max|aura|color|layer|tip|gmnotes|type|pc|npc|pt|side|party)(?1|2|3|4)?(?<=(?:bar|max)\d|(?:aura|color)[1,2]|(?:layer|tip|gmnotes|type|pc|npc|pt|side|party))$/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)) + .reduce((m, group) => { + m = [...m, ...group]; + return m; + }, []) + .filter(t => typeof t !== 'undefined'); + }; + let allowDupes = false; + 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 = []; + } + allowDupes = allowDupes || /^(\()?{&\s*\+/.test(m); + 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 = (allowDupes ? unpackGroups(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 (allowDupes || !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(/\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(/\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\((?selected)[|.](?[^\s[|.)]+?)(?:[|.](?[^\s.[|]+?)){0,1}(?:\[(?[^\]]*?)]){0,1}\s*\)/gi; + const fetchrptgselrx = /at\((?selected)[|.](?
    [^\s.|]+?)[|.]\[\s*(?.+?)\s*]\s*[|.](?[^[\s).]+?)(?:[|.](?[^\s.[)]+?)){0,1}(?:\[(?[^\]]*?)]){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', 'bar4'].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, '
    \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 => `• ${k} : ${depCheck.optfailures[k]}`).join('
    '); + contents = `${apiproject} 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:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + } + if (!depCheck.passed) { + failures = Object.keys(depCheck.failures).map(k => `• ${k} : ${depCheck.failures[k]}`).join('
    '); + contents = `${apiproject} requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + 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); } } +/* */ diff --git a/SelectManager/SelectManager.js b/SelectManager/SelectManager.js index 4e7f5672e..536106535 100644 --- a/SelectManager/SelectManager.js +++ b/SelectManager/SelectManager.js @@ -2,9 +2,9 @@ ========================================================= Name : SelectManager GitHub : https://github.com/TimRohr22/Cauldron/tree/master/SelectManager -Roll20 Contact : timmaugh && TheAaron -Version : 1.1.10 -Last Update : 6 OCT 2025 +Roll20 Contact : timmaugh && The Aaron +Version : 1.1.13 +Last Update : 27 DEC 2025 ========================================================= */ var API_Meta = API_Meta || {}; @@ -16,12 +16,12 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars // VERSION // ================================================== const apiproject = 'SelectManager'; - const version = '1.1.10'; + const version = '1.1.13'; 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(1759763868181); + const vd = new Date(1766852681848); 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) { @@ -226,14 +226,15 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars return Campaign().get('playerpageid'); }; - const getTokens = (query, pid, owner = true) => { - if (pid === 'API') pid = preservedMsgObj[maintrigger].playerid; - let pageid = getPageForPlayer(pid); + const asGM = (msg) => playerIsGM(msg.playerid) || msg.hasOwnProperty('reactionProps'); + const getTokens = (query, msg, owner = true) => { + if (msg.playerid === 'API') msg.playerid = preservedMsgObj[maintrigger].playerid; + let pageid = getPageForPlayer(msg.playerid); 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)); + .filter(t => t.get('layer') === 'objects' || asGM(msg)); if (owner) { - alltokens = alltokens.filter(t => playerIsGM(pid) || playersCanUseIDs() || playerCanControl(t, pid)); + alltokens = alltokens.filter(t => asGM(msg) || playersCanUseIDs() || playerCanControl(t, msg.playerid)); } let tokens = [(alltokens.filter(t => t.id === query)[0] || alltokens.filter(t => t.get('name') === query)[0])] @@ -559,6 +560,14 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars if (!control.length) return true; return !control.filter(s => s.length && !playerIsGM(s)).length; }; + const isParty = (obj = { get: () => { return ''; } }) => { + let char = ( + obj.get('represents') && obj.get('represents').length + ? getObj('character', obj.get('represents') || { get: function () { return ''; } }) + : obj + ); + return char.get('inParty'); + }; const internalTestLib = { 'int': (v) => +v === +v && parseInt(parseFloat(v, 10), 10) == v, 'num': (v) => +v === +v, @@ -699,6 +708,14 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars tksetting = t.get('currentSide'); comp = [tksetting, c.value]; } + break; + case 'party': + if (t.get('type') === 'graphic' && t.get('subtype') === 'token' && t.get('layer') === 'objects') { + test = '='; + comp = [isParty(t), true]; + } + break; + break; default: return false; @@ -723,10 +740,12 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars 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 injectrx = /(\()?{&\s*inject\s+([^}]+?)\s*}((?<=\({&\s*inject\s+([^}]+?)\s*})\)|\1)/gi; + // const selectrx = /(\()?{&\s*select\s+([^}]+?)\s*}((?<=\({&\s*select\s+([^}]+?)\s*})\)|\1)/gi; + 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 = /^(?\+|-)(?@|\*|#)?(?[^\s><=!~]+)(?:\s*$|\s*(?>=|<=|~|!~|=|!=|<|>|in(?=\s+\[[^\]]+\]))\s*(?.+)$)/; - const typeitemrx = /^(?bar|max|aura|color|layer|tip|gmnotes|type|pc|npc|pt|side)(?1|2|3|4)?(?<=(?:bar|max)\d|(?:aura|color)[1,2]|(?:layer|tip|gmnotes|type|pc|npc|pt|side))$/i; + const typeitemrx = /^(?bar|max|aura|color|layer|tip|gmnotes|type|pc|npc|pt|side|party)(?1|2|3|4)?(?<=(?:bar|max)\d|(?:aura|color)[1,2]|(?:layer|tip|gmnotes|type|pc|npc|pt|side|party))$/i; const inject = (msg, status, msgId/*, notes*/) => { const layerCriteria = (criteria) => { return criteria.filter(c => c.type === 'layer').length ? true : false; @@ -768,13 +787,14 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars }; const unpackGroups = (array) => { return array - .map(l => getTokens(l, msg.playerid)) + .map(l => getTokens(l, msg)) .reduce((m, group) => { m = [...m, ...group]; return m; }, []) .filter(t => typeof t !== 'undefined'); }; + let allowDupes = false; const replaceOps = (rx, rxtype) => { rx.lastIndex = 0; msg.content = msg.content.replace(rx, (m, padding, group) => { @@ -783,6 +803,7 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars } else if (rxtype === 'select') { msg.selected = []; } + allowDupes = allowDupes || /^(\()?{&\s*\+/.test(m); let identifiers = getGroups(group) .reduce((m, v) => { if (criteriarx.test(v) && !findObjs({ id: v }).length) { @@ -816,7 +837,7 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars 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') + identifiers.selections = (allowDupes ? unpackGroups(identifiers.selections) : uniqueArrayByProp(unpackGroups(identifiers.selections), 'id')) .filter(t => { return identifiers.criteria.every(c => evaluateCriteria(c, t, msgId)); }); @@ -824,7 +845,7 @@ const SelectManager = (() => { //eslint-disable-line no-unused-vars 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)) { + if (allowDupes || !m.map(mt => mt._id).includes(t._id)) { m.push(t); } return m; diff --git a/SelectManager/script.json b/SelectManager/script.json index 4effb275a..4e456d832 100644 --- a/SelectManager/script.json +++ b/SelectManager/script.json @@ -1,7 +1,7 @@ { "name": "SelectManager", "script": "SelectManager.js", - "version": "1.1.12", + "version": "1.1.13", "description": "SelectManager stores the selected, who, and playerid properties from the last user-generated message (as opposed to API generated), and makes them available for another script to retrieve. This solves the problem of an API-generated message not having the original array of selected tokens, for instance. \r\rIt also provides a !forselected handle to iterate over the selected tokens, firing off an individual call to another script for each token in turn, making each the selected token.\r\rFinally, it offers a way to virtually select tokens for the message, or to inject tokens into the selected token array.\r\rFor more information, see the original thread in the API forum:\r\r[SelectManager Forum Thread](https://app.roll20.net/forum/post/9817678/script-selectmanager-update-brings-forselected-iteration-and-gives-user-new-control-to-give-selected-tokens-back-to-api-generated-messages)\r\rOr read about the full set of meta-scripts available: \r\r[Meta Toolbox Forum Thread](https://app.roll20.net/forum/post/10005695/script-set-the-meta-toolbox)", "authors": "timmaugh, The Aaron", "roll20userid": "5962076, 104025", @@ -36,6 +36,7 @@ "1.1.8", "1.1.9", "1.1.10", - "1.1.11" + "1.1.11", + "1.1.12" ] }