From b99e047499d09235d9da660c49083baa38f455e1 Mon Sep 17 00:00:00 2001 From: timmaugh Date: Fri, 6 Sep 2024 09:52:43 -0400 Subject: [PATCH] Script Updates Sept 6 ZeroFrame Version Fix MTB - dependencies updated APIL - recursive parser bug fix --- APILogic/2.0.9/APILogic.js | 778 ++++++++++ APILogic/APILogic.js | 28 +- APILogic/script.json | 5 +- MetaScriptToolbox/0.0.2/MetaScriptToolbox.js | 234 +++ MetaScriptToolbox/MetaScriptToolbox.js | 22 +- MetaScriptToolbox/script.json | 6 +- ZeroFrame/1.2.2/ZeroFrame.js | 1348 ++++++++++++++++++ ZeroFrame/ZeroFrame.js | 8 +- ZeroFrame/script.json | 5 +- 9 files changed, 2402 insertions(+), 32 deletions(-) create mode 100644 APILogic/2.0.9/APILogic.js create mode 100644 MetaScriptToolbox/0.0.2/MetaScriptToolbox.js create mode 100644 ZeroFrame/1.2.2/ZeroFrame.js diff --git a/APILogic/2.0.9/APILogic.js b/APILogic/2.0.9/APILogic.js new file mode 100644 index 0000000000..a2eb663160 --- /dev/null +++ b/APILogic/2.0.9/APILogic.js @@ -0,0 +1,778 @@ +/* +========================================================= +Name : APILogic +GitHub : https://github.com/TimRohr22/Cauldron/tree/master/APILogic +Roll20 Contact : timmaugh +Version : 2.0.9 +Last Update : 5 SEP 2024 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.APILogic = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ + try { throw new Error(''); } catch (e) { API_Meta.APILogic.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); } +} + +const APILogic = (() => { + // ================================================== + // VERSION + // ================================================== + const apiproject = 'APILogic'; + API_Meta[apiproject].version = '2.0.9'; + const schemaVersion = 0.1; + const vd = new Date(1725559091022); + 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: + /* break; // intentional dropthrough */ /* falls through */ + + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + version: schemaVersion, + }; + break; + } + } + }; + 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; + }; + + // REGEXES ============================================== + const defblockrx = /(\$?){&\s*define\s*/i, + definitionrx = /\(\s*\[\s*(?.+?)\s*]\s*('|"|`?)(?.*?)\2\)\s*/i, + ifrx = /(\()?{&\s*if(?=\(|\s+|!)\s*/i, + elseifrx = /(\()?{&\s*elseif(?=\(|\s+|!)\s*/i, + elserx = /(\()?{&\s*else\s*(?=})/i, + endrx = /(\()?{&\s*end\s*}((?<=\({&\s*end\s*})\)|\1)/i; + // FORMERLY in IFTREEPARSER ============================= + const groupopenrx = /^\s*(?!?)\s*\((?!{&\d+}\))\s*/, + namerx = /^\[(?[^\s]+?)]\s*/i, + comprx = /^(?(?:>=|<=|~|!~|=|!=|<|>))\s*/, + operatorrx = /^(?(?:&&|\|\|))\s*/, + groupendrx = /^\)\s*/, + ifendrx = /^\s*}/, + ifendparenrx = /^\s*}\)/, + textrx = /^(?!?)\s*(`|'|"?)(?\({&\d+}\)|.+?)\2\s*(?=!=|!~|>=|<=|[=~><]|&&|\|\||\)|})/; + // TOKEN MARKERS ======================================== + const iftm = { rx: ifrx, type: 'if' }, + elseiftm = { rx: elseifrx, type: 'elseif' }, + elsetm = { rx: elserx, type: 'else' }, + endtm = { rx: endrx, type: 'end' }, + eostm = { rx: /$/, type: 'eos' }, + groupendtm = { rx: groupendrx, type: 'groupend' }, + ifendtm = { rx: ifendrx, type: 'mainconditions' }, + operatortm = { rx: operatorrx, type: 'operator' }, + texttm = { rx: textrx, type: 'text' }, + defblocktm = { rx: defblockrx, type: 'defblock' }, + comptm = { rx: comprx, type: 'comp' }; + + // END TOKEN REGISTRY =================================== + const endtokenregistry = { + main: [eostm], + if: [elseiftm, elsetm, endtm], + elseif: [elseiftm, elsetm, endtm], + else: [endtm], + mainconditions: [ifendtm], + group: [groupendtm], + }; + + const nestlog = (stmt, ilvl = 0, logcolor = '', boolog = false) => { + if (isNaN(ilvl)) { + ilvl = 0; + log(`Next statement fed a NaN value for the indentation.`); + } + if ((state[apiproject] && state[apiproject].logging === true) || boolog) { + let l = `${Array(ilvl + 1).join("==")}${stmt}`; + if (logcolor) { + // l = /:/.test(l) ? `${l.replace(/:/, ':')}` : `${l}`; + } + log(l); + } + }; + + const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); }; + const assertstart = rx => new RegExp(`^${rx.source}`, rx.flags); + const getfirst = (cmd, ...args) => { + // pass in objects of form: {type: 'text', rx: /regex/} + // return object of form : {regex exec object with property 'type': 'text'} + + let ret = {}; + let r; + args.find(a => { + r = a.rx.exec(cmd); + if (r && (!ret.length || r.index < ret.index)) { + ret = Object.assign(r, { type: a.type }); + } + a.lastIndex = 0; + }, ret); + return ret; + }; + const internalTestLib = { + 'int': (v) => +v === +v && parseInt(parseFloat(v, 10), 10) == v, + 'num': (v) => +v === +v, + 'tru': (v) => v == true + }; + + // ================================================== + // PARSER PROCESSING + // ================================================== + const ifTreeParser = (msg, msgstate, status, notes) => { + + class TextToken { + constructor() { + this.type = 'text'; + this.value = ''; + } + } + class IfToken { + constructor() { + this.type = 'if'; + this.conditions = []; + this.contents = []; + this.else = {}; + } + } + class GroupToken { + constructor() { + this.type = 'group'; + this.name = ''; + this.contents = []; + this.next = ''; + this.negate = false; + } + } + class ConditionToken { + constructor() { + this.type = 'condition'; + this.contents = []; + this.next = ''; + this.negate = false; + } + } + + const val = c => { + // expects an object in the form of: + // { cmd: text, indent: #, type: main/if/elseif/else, overallindex: #} + let tokens = []; // main output array + let logcolor = 'aqua'; + let loopstop = false; + let tokenres = {}; + let index = 0; + let loopindex = 0; + nestlog(`VAL BEGINS`, c.indent, logcolor, msgstate.logging); + while (!loopstop) { + loopindex = index; + if (assertstart(ifrx).test(c.cmd.slice(index))) { + status.push('changed'); + tokenres = getIfToken({ cmd: c.cmd.slice(index), indent: c.indent + 1, overallindex: c.overallindex + index }); + } else { + tokenres = getTextToken({ cmd: c.cmd.slice(index), indent: c.indent + 1, overallindex: c.overallindex + index }); + } + if (tokenres) { + if (tokenres.error) { return tokenres; } + tokens.push(tokenres.token); + index += tokenres.index; + } + if (loopindex === index) { // nothing detected, loop never ends + return { error: `Unexpected token at ${c.overallindex + index}.` }; + } + loopstop = (getfirst(c.cmd.slice(index), ...endtokenregistry[c.type]).index === 0); + } + nestlog(`VAL ENDS`, c.indent, logcolor, msgstate.logging); + return { tokens: tokens, index: index }; + }; + const getTextToken = (c) => { + let logcolor = 'lawngreen'; + nestlog(`TEXT INPUT: ${c.cmd}`, c.indent, logcolor, msgstate.logging); + let markers = []; + c.looptype = c.looptype || ''; + switch (c.looptype) { + case 'def': + markers = [defblocktm, eostm]; + break; + case 'eval': + markers = [evaltm, evalendtm, eostm]; + break; + case 'eval1': + markers = [eval1tm, eval1endtm, eostm]; + break; + default: + markers = [iftm, elseiftm, elsetm, endtm, eostm]; + break; + } + let res = getfirst(c.cmd, ...markers); + let index = res.index; + let token = new TextToken(); + token.value = c.cmd.slice(0, index); + nestlog(`TEXT KEEPS: ${token.value}`, c.indent, logcolor, msgstate.logging); + //log(`
${syntaxHighlight(token)}
`); + return { token: token, index: index }; + }; + const getIfToken = (c) => { + + // receives object in the form of: + // {cmd: command line slice, indent: #, type: if/else} + let logcolor = 'yellow'; + let res = getfirst(c.cmd, iftm, elseiftm, elsetm); + // one of these should be at the 0 index position + if (res && res.index === 0) { + nestlog(`IF INPUT: ${c.cmd}`, c.indent, logcolor, msgstate.logging); + let token = new IfToken(); + let index = res[0].length; + + // groups and conditions + if (['if', 'elseif'].includes(res.type)) { + let condres = getConditions({ cmd: c.cmd.slice(index), indent: c.indent + 1, type: 'mainconditions', overallindex: c.overallindex + index }); + if (condres.error) { return condres; } + token.conditions = condres.tokens; + index += condres.index; + } + + // closing bracket of if/elseif/else tag + let ifendres = (res[1] ? ifendparenrx : ifendrx).exec(c.cmd.slice(index)); + if (!ifendres) { // no end brace or the parens do not match + return { error: `Unexpected token at ${c.overallindex + index}. Expected end of logic structure ('}'), but saw: ${c.cmd.slice(index, index + 10)}` }; + } + index += ifendres[0].length; + + // text content and nested ifs + nestlog(`BUILDING CONTENT: ${c.cmd.slice(index)}`, c.indent + 1, 'lightseagreen', msgstate.logging); + let contentres = val({ cmd: c.cmd.slice(index), indent: c.indent + 2, type: res.type, overallindex: c.overallindex + index }); + if (contentres.error) return contentres; + token.contents = contentres.tokens; + index += contentres.index; + nestlog(`ENDING CONTENT: ${c.cmd.slice(index)}`, c.indent + 1, 'lightseagreen', msgstate.logging); + + // else cases + let firstelseres = getfirst(c.cmd.slice(index), ...endtokenregistry[res.type]); + if (firstelseres && firstelseres.type !== 'end' && firstelseres.index === 0) { + nestlog(`BUILDING ELSE: ${c.cmd.slice(index)}`, c.indent + 1, 'lightsalmon', msgstate.logging); + let elseres = getIfToken({ cmd: c.cmd.slice(index), indent: c.indent + 2, type: firstelseres.type, overallindex: c.overallindex + index }); + token.else = elseres.token || []; + index += elseres.index; + nestlog(`ENDING ELSE: ${c.cmd.slice(index)}`, c.indent + 1, 'lightsalmon', msgstate.logging); + } + // end token (only for full IF blocks) + if (res.type === 'if') { + let endres = assertstart(endrx).exec(c.cmd.slice(index)); + if (!endres) { + return { error: `Unexpected token at ${c.overallindex + index}. Expected logical structure (END), but saw: ${c.cmd.slice(index, index + 10)}` }; + } + index += endres[0].length; + } + nestlog(`IF OUTPUT: ${JSON.stringify(token)}`, c.indent, logcolor, msgstate.logging); + //log(`
${syntaxHighlight(token)}
`); + return { token: token, index: index }; + } else { + return { error: `Unexpected token at ${c.overallindex + index}. Expected a logic structure (IF, ELSEIF, or ELSE), but saw: ${c.cmd.slice(index, index + 10)}` }; + } + }; + const getConditions = (c) => { + // expects object in the form {cmd: text, indent: #, type: mainconditions/group, overallindex: #} + let tokens = []; // main output array + let logcolor = 'darkorange'; + let loopstop = false; + let tokenres = {}; + let index = 0; + let loopindex = 0; + nestlog(`GETCONDITIONS BEGINS`, c.indent, logcolor, msgstate.logging); + while (!loopstop) { + loopindex = index; + if (groupopenrx.test(c.cmd.slice(index))) { + tokenres = getGroupToken({ cmd: c.cmd.slice(index), indent: c.indent + 1, overallindex: c.overallindex + index }); + } else { + tokenres = getConditionToken({ cmd: c.cmd.slice(index), indent: c.indent + 1, overallindex: c.overallindex + index }); + } + if (tokenres) { + if (tokenres.error) return tokenres; + tokens.push(tokenres.token); + index += tokenres.index; + + } + if (loopindex === index) { // no token found, loop won't end + return { error: `Unexpected token at ${c.overallindex + index}.` }; + } + loopstop = (getfirst(c.cmd.slice(index), ...endtokenregistry[c.type]).index === 0); + } + nestlog(`GETCONDITIONS ENDS`, c.indent, logcolor, msgstate.logging); + return { tokens: tokens, index: index }; + }; + const getGroupToken = (c) => { + let logcolor = 'violet'; + let index = 0; + let groupres = groupopenrx.exec(c.cmd); + if (groupres) { + nestlog(`GROUP INPUT: ${c.cmd}`, c.indent, logcolor, msgstate.logging); + index += groupres[0].length; + let token = new GroupToken(); + // negation + token.negate = !!groupres.groups.negation; + // name + let nameres = namerx.exec(c.cmd.slice(index)); + if (nameres) { + token.name = nameres.groups.groupname; + index += nameres[0].length; + } + + // text content and nested groups + nestlog(`BUILDING CONTENT: ${c.cmd.slice(index)}`, c.indent + 1, 'lightseagreen', msgstate.logging); + let contentres = getConditions({ cmd: c.cmd.slice(index), indent: c.indent + 2, type: 'group', overallindex: c.overallindex + index }); + if (contentres) { + if (contentres.error) { return contentres; } + token.contents = contentres.tokens; + index += contentres.index; + } + nestlog(`ENDING CONTENT: ${c.cmd.slice(index)}`, c.indent + 1, 'lightseagreen', msgstate.logging); + + // closing paren of group + let groupendres = groupendrx.exec(c.cmd.slice(index)); + if (!groupendres) { + return { error: `Unexpected token at ${c.overallindex + index}. Expected the end of a group but saw: ${c.cmd.slice(index, index + 10)}` }; + } + index += groupendres[0].length; + + // connecting operator + let operatorres = operatorrx.exec(c.cmd.slice(index)); + if (operatorres) { + token.next = operatorres.groups.operator; + index += operatorres[0].length; + } + + nestlog(`GROUP OUTPUT: ${JSON.stringify(token)}`, c.indent, logcolor, msgstate.logging); + //log(`
${syntaxHighlight(token)}
`); + return { token: token, index: index }; + } + }; + const getConditionToken = (c) => { + let logcolor = 'white'; + let index = 0; + let firstargres = getfirst(c.cmd, comptm, texttm); + if (firstargres) { + nestlog(`CONDITION INPUT: ${c.cmd}`, c.indent, logcolor, msgstate.logging); + let token = new ConditionToken(); + if (firstargres.type === 'comp') { + firstargres = getfirst(' =', texttm); + } else { + index += firstargres[0].length; + } + token.contents.push(firstargres); + + let compres = comprx.exec(c.cmd.slice(index)); + if (compres) { + index += compres[0].length; + let secondargres = getfirst(c.cmd.slice(index), groupendtm, ifendtm, operatortm, texttm); + if (secondargres && ![groupendtm.type, ifendtm.type, operatortm.type].includes(secondargres.type)) { + index += secondargres[0].length; + } else { // comparison operator with no second arg + secondargres = getfirst(' =', texttm); + } + token.contents.push(secondargres); + token.type = compres.groups.operator; + } + // connecting operator + let operatorres = operatorrx.exec(c.cmd.slice(index)); + if (operatorres) { + token.next = operatorres.groups.operator; + index += operatorres[0].length; + } + nestlog(`CONDITION OUTPUT: ${JSON.stringify(token)}`, c.indent, logcolor, msgstate.logging); + //log(`
${syntaxHighlight(token)}
`); + return { token: token, index: index }; + + } else { // no first arg found, return an error + return { error: `Unexpected token at ${c.overallindex + index}. Expected a condition argument but saw: {c.cmd.slice(index, index+10)}` }; + } + }; + + const getTermToken = (c) => { + // receives object in the form of: + // {cmd: command line slice, indent: #} + let logcolor = 'yellow'; + let index = 0; + let res = assertstart(definitionrx).exec(c.cmd); + if (res) { + nestlog(`TERM INPUT: ${c.cmd}`, c.indent, logcolor, msgstate.logging); + let tokens = []; + let loopstop = false; + while (!loopstop) { + tokens.push({ term: res.groups.term, definition: res.groups.definition }); + nestlog(`TERM DEFINED: ${res.groups.term} = ${res.groups.definition}`, c.indent + 1, logcolor, msgstate.logging); + index += res[0].length; + res = assertstart(definitionrx).exec(c.cmd.slice(index)); + if (!res) loopstop = true; + } + + nestlog(`TERM OUTPUT: ${JSON.stringify(tokens)}`, c.indent, logcolor, msgstate.logging); + return { token: tokens, index: index }; + } else { + return { error: `Unexpected token at ${c.overallindex + index}. Expected a term and definition, but saw: ${c.cmd.slice(index, index + 10)}` }; + } + }; + const defval = c => { + // expects an object in the form of: + // { cmd: text, indent: #, defs: [] } + let tokens = []; // main text output array + let defs = c.defs; // main definition array + let logcolor = 'aqua'; + let loopstop = false; + let defendres = {}; + let tokenres = {}; + let index = 0; + let loopindex = 0; + let res; + nestlog(`DEFVAL BEGINS`, c.indent, logcolor, msgstate.logging); + while (!loopstop) { + loopindex = index; + if (assertstart(defblockrx).test(c.cmd.slice(index))) { + status.push('changed'); + res = assertstart(defblockrx).exec(c.cmd.slice(index)); + index += res[0].length; + tokenres = getTermToken({ cmd: c.cmd.slice(index), indent: c.indent + 1 }); + } else { + tokenres = getTextToken({ cmd: c.cmd.slice(index), indent: c.indent + 1, looptype: 'def' }); + } + if (tokenres) { + if (tokenres.error) { + return tokenres; + } + index += tokenres.index; + if (tokenres.token.type === 'text') { + tokens.push(tokenres.token); + } else { + defendres = (res[1] ? ifendparenrx : ifendrx).exec(c.cmd.slice(index)); + if (!defendres) { // no end brace or the parens do not match + return { error: `Unexpected token at ${c.overallindex + index}. Expected end of definition ('}'), but saw: ${c.cmd.slice(index, index + 10)}` }; + } + index += defendres[0].length; + defs = [...defs, ...tokenres.token]; + } + } + if (loopindex === index) { // nothing detected, loop never ends + return { error: `Unexpected token at ${c.overallindex + index}.` }; + } + loopstop = (getfirst(c.cmd.slice(index), ...endtokenregistry[c.type]).index === 0); + } + + // get non-definitional text back into a string + let nondeftext = []; + tokens.forEach(t => nondeftext.push(t.value)); + let newcmd = nondeftext.join(''); + // replace all term/defs + defs.forEach(d => { + newcmd = newcmd.replace(new RegExp(escapeRegExp(d.term), 'g'), d.definition); + }); + nestlog(`DEFVAL ENDS`, c.indent, logcolor, msgstate.logging); + return { cmd: newcmd, defs: defs }; + }; + + const checkWellFormed = (cmd) => { + let ifarray = [], + index = 0, + nextstructure = getfirst(cmd, iftm, elseiftm, elsetm, endtm, eostm), + retObj = { wellformed: true, error: '', position: 0 }; + while (index < cmd.length && retObj.wellformed) { + index += nextstructure.index; + switch (nextstructure.type) { + case 'if': + ifarray.push(true); + break; + case 'elseif': + case 'else': + if (!ifarray.length) { + retObj = { wellformed: false, error: `${nextstructure.type.toUpperCase()} without IF at position ${index}` }; + } else { + if (!ifarray[ifarray.length - 1]) { + retObj = { wellformed: false, error: `${nextstructure.type.toUpperCase()} after ELSE at position ${index}` }; + } else { + if (nextstructure.type === 'else') { + ifarray[ifarray.length - 1] = false; + } + } + } + break; + case 'end': + if (!ifarray.length) { + retObj = { wellformed: false, error: `END without IF at position ${index}` }; + } else { + ifarray.pop(); + } + break; + case 'eos': + + break; + } + if (retObj.wellformed && nextstructure.type !== 'eos') { + index += nextstructure[0].length; + nextstructure = getfirst(cmd.slice(index), iftm, elseiftm, elsetm, endtm, eostm); + } + + } + return retObj; + }; + + const main = (msg) => { + let retObj = {}; + + // DEFINITION BLOCK DETECTION + let defcmd = defval({ cmd: msg.content, indent: 0, type: 'main', overallindex: 0, defs: [...(msg.definitions || [])] }); + if (!defcmd.cmd) return { tokens: [], error: defcmd.error }; + if (defcmd.defs.length) msg.definitions = defcmd.defs; + // WELL-FORMED CHECK + let wf = checkWellFormed(defcmd.cmd); + if (!wf.wellformed) return { tokens: [], error: wf.error }; + // LOGIC PARSING + retObj = val({ cmd: defcmd.cmd, indent: 0, type: 'main', overallindex: 0 }); + + return retObj; + } + return main(msg); + }; + const reconstructCommandLine = (o, msgstate, status, notes) => { + const grouplib = {}; + const typeProcessor = { + '=': (t) => t.contents[0].value == t.contents[1].value, + '!=': (t) => t.contents[0].value != t.contents[1].value, + '~': (t) => t.contents[0].value.includes(t.contents[1].value), + '!~': (t) => !t.contents[0].value.includes(t.contents[1].value), + '>': (t) => (internalTestLib.num(t.contents[0].value) ? Number(t.contents[0].value) : t.contents[0].value) > (internalTestLib.num(t.contents[1].value) ? Number(t.contents[1].value) : t.contents[1].value), + '>=': (t) => (internalTestLib.num(t.contents[0].value) ? Number(t.contents[0].value) : t.contents[0].value) >= (internalTestLib.num(t.contents[1].value) ? Number(t.contents[1].value) : t.contents[1].value), + '<': (t) => (internalTestLib.num(t.contents[0].value) ? Number(t.contents[0].value) : t.contents[0].value) < (internalTestLib.num(t.contents[1].value) ? Number(t.contents[1].value) : t.contents[1].value), + '<=': (t) => (internalTestLib.num(t.contents[0].value) ? Number(t.contents[0].value) : t.contents[0].value) <= (internalTestLib.num(t.contents[1].value) ? Number(t.contents[1].value) : t.contents[1].value) + } + + const resolveCondition = (t) => { + // expects condition token + // each item in t.contents should be a regex result also with a property type: 'sheetitem', 'rptgitem', 'text' + // t.type :: 'condition', '=', '!=', etc. + // negation is at t.contents[#].groups.negation + // comparable or usable text is different for text vs sheet item vs rpt item + // internalTestLib moved to outer scope + + t.contents.forEach(item => { + item.metavalue = true; + switch (item.type) { + case 'text': + item.groups.argtext = item.groups.argtext + .replace(/\$\[\[(\d+)]]/g, ((r, g1) => o.parsedinline[g1].value || 0)) + .replace(/\({&(\d+)}\)/, ((r, g1) => o.parsedinline[g1].value || 0)); + if (grouplib.hasOwnProperty(item.groups.argtext)) { + if (grouplib[item.groups.argtext]) item.value = true; + else { + item.value = false; + item.metavalue = false; + } + } else { + item.value = item.groups.argtext; + } + break; + default: + log('Unknown token type in reconstruction'); + break; + } + if (item.groups.negation === '!') { + item.value = !item.value; + item.metavalue = !item.metavalue; + } + }) + if (t.type === 'condition') { + // single arg tests: exists, is integer, named condition, etc. + t.value = t.contents[0].metavalue && t.contents[0].value; + return t; + } else { + // two arg tests: =, !=, >, etc. + t.value = t.contents[0].metavalue && typeProcessor[t.type](t); + } + return t; + }; + + const areConditionsTruthy = c => { + // expects conditions array + let logcolor = 'lightseagreen'; + let groupname = ''; + let negate = false; + let res; + c.memo = c.hasOwnProperty("memo") ? c.memo : { value: false, next: '||' }; + nestlog(`CONDITIONS TEST BEGINS`, c.indent, logcolor, msgstate.logging); + let o = c.tokens.reduce((m, v, i) => { + if ((!m.value && m.next === '&&') || (m.value && m.next === '||')) { + nestlog(`==TEST SKIPPED`, c.indent, logcolor, msgstate.logging); + } else { + if (v.type === 'group') { + nestlog(`==AND-GROUP DETECTED: ${v.name || 'no name'}`, c.indent, logcolor, msgstate.logging); + groupname = v.name; + negate = v.negate; + res = areConditionsTruthy({ tokens: v.contents, indent: c.indent + 1, memo: { ...m } }); + v.value = res.value; + if (groupname) { + grouplib[groupname] = v.value; + } + if (negate) v.value = !v.value; + } else { + nestlog(`==AND-CONDITION DETECTED: lhs>${v.contents[0]} type>${v.type} rhs>${v.contents[1] || ''}`, c.indent, logcolor, msgstate.logging); + ret = resolveCondition(v); + v.value = ret.value; + } + nestlog(`==VALUE: ${v.value}`, c.indent, logcolor, msgstate.logging); + m.value = m.next === '&&' ? m.value && v.value : m.value || v.value; + } + m.next = v.next; + nestlog(`==LOOP END MEMO VALUE: ${m.value}, ${m.next}`, c.indent, logcolor, msgstate.logging); + return m; + }, c.memo); + + nestlog(`CONDITIONS TEST ENDS: Conditions are ${o.value}, ${o.next}`, c.indent, logcolor, msgstate.logging); + return o; + }; + + const processContents = c => { + // expects contents array + let logcolor = 'aqua'; + nestlog(`PROCESS CONTENT BEGINS`, c.indent, logcolor, msgstate.logging); + let tokens = c.tokens.reduce((m, v, i) => { + nestlog(`==TOKEN ${i}: ${JSON.stringify(v)}`, c.indent, 'violet', msgstate.logging); + if (v.type === 'text') { + nestlog(`====DETECTED TEXT: ${v.value}`, c.indent, 'lawngreen', msgstate.logging); + m.push(v.value); + } else if (v.type === 'if') { + nestlog(`====DETECTED IF`, c.indent, 'yellow', msgstate.logging); + if (!v.conditions.length || areConditionsTruthy({ tokens: v.conditions, indent: c.indent + 1 }).value) { + nestlog(`======TRUE CASE`, c.indent, 'darkorange', msgstate.logging); + m.push(processContents({ tokens: v.contents, indent: c.indent + 1 }).join('')); + } else if (v.else) { + nestlog(`======TESTING ELSE CASE`, c.indent, 'darkorange', msgstate.logging); + m.push(processContents({ tokens: [v.else], indent: c.indent + 1 }).join('')); + } + } + nestlog(`==END OF TOKEN`, c.indent, 'violet', msgstate.logging); + return m; + }, []); + nestlog(`PROCESS CONTENT ENDS`, c.indent, logcolor, msgstate.logging); + return tokens; + }; + let content = processContents({ tokens: o.tokens, indent: 0 }).join(''); + return { content: content, logicgroups: grouplib }; + }; + + 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; + }; + + // ================================================== + // TEST CONSTRUCTS + // ================================================== + const testConstructs = (c) => { + let result = ifrx.test(c) || defblockrx.test(c); + ifrx.lastIndex = 0; + defblockrx.lastIndex = 0; + + return result; + }; + + // ================================================== + // HANDLE INPUT + // ================================================== + const handleInput = (msg, msgstate = {}) => { + let funcret = { runloop: false, status: 'unchanged', notes: '' }; + if (msg.type !== 'api' || !testConstructs(msg.content)) { + if (!msg.definitions || !msg.definitions.length) return funcret; + let termrx = new RegExp(msg.definitions.map(d => escapeRegExp(d.term)).join('|'), 'gm'); + if (!termrx.test(msg.content)) return funcret; + } + if (!Object.keys(msgstate).length && scriptisplugin) return funcret; + let status = []; + let notes = []; + const linebreak = '({&br-al})'; + msg.content = msg.content.replace(/\n/g, linebreak); + + msg.logicgroups = msg.logicgroups || {}; + msg.definitions = msg.definitions || []; + let o = ifTreeParser(msg, msgstate, status, notes); + if (o.error) { + status.push('unresolved'); + notes.push(o.error); + log(o.error); + log(msg.content); + return condensereturn(funcret, status, notes); + } + if (o.tokens) { + // reconstruct command line + o.playerid = msg.playerid; + o.logicgroups = msg.logicgroups; + o.parsedinline = msg.parsedinline || []; + let reconstruct = reconstructCommandLine(o, msgstate, status, notes); + if (msg.content !== reconstruct.content) status.push('chnaged'); + msg.content = reconstruct.content; + msg.content = msg.content.replace(new RegExp(escapeRegExp(linebreak), 'g'), '
\n'); + msg.logicgroups = reconstruct.logicgroups; + } else { + status.push('unresolved'); + notes.push('Unexpected error encountered. Unable to reconstruct command line.'); + return condensereturn(funcret, status, notes); + } + return condensereturn(funcret, status, notes); + }; + + let scriptisplugin = false; + const apilogic = (m, s) => handleInput(m, s); + on('chat:message', handleInput); + on('ready', () => { + versionInfo(); + logsig(); + scriptisplugin = (typeof ZeroFrame !== `undefined`); + if (typeof ZeroFrame !== 'undefined') { + ZeroFrame.RegisterMetaOp(apilogic, { priority: 70, handles: ['apil', 'logic'] }); + } + }); + + return { + } +})(); +{ try { throw new Error(''); } catch (e) { API_Meta.APILogic.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.APILogic.offset); } } diff --git a/APILogic/APILogic.js b/APILogic/APILogic.js index bb86fc5bdf..a2eb663160 100644 --- a/APILogic/APILogic.js +++ b/APILogic/APILogic.js @@ -3,8 +3,8 @@ Name : APILogic GitHub : https://github.com/TimRohr22/Cauldron/tree/master/APILogic Roll20 Contact : timmaugh -Version : 2.0.8 -Last Update : 26 Jan 2023 +Version : 2.0.9 +Last Update : 5 SEP 2024 ========================================================= */ var API_Meta = API_Meta || {}; @@ -18,9 +18,9 @@ const APILogic = (() => { // VERSION // ================================================== const apiproject = 'APILogic'; - API_Meta[apiproject].version = '2.0.8'; + API_Meta[apiproject].version = '2.0.9'; const schemaVersion = 0.1; - const vd = new Date(1674771283027); + const vd = new Date(1725559091022); 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) { @@ -76,14 +76,14 @@ const APILogic = (() => { elserx = /(\()?{&\s*else\s*(?=})/i, endrx = /(\()?{&\s*end\s*}((?<=\({&\s*end\s*})\)|\1)/i; // FORMERLY in IFTREEPARSER ============================= - const groupopenrx = /^\s*(?!?)\s*\(\s*/, + const groupopenrx = /^\s*(?!?)\s*\((?!{&\d+}\))\s*/, namerx = /^\[(?[^\s]+?)]\s*/i, comprx = /^(?(?:>=|<=|~|!~|=|!=|<|>))\s*/, operatorrx = /^(?(?:&&|\|\|))\s*/, groupendrx = /^\)\s*/, ifendrx = /^\s*}/, ifendparenrx = /^\s*}\)/, - textrx = /^(?!?)\s*(`|'|"?)(?.+?)\2\s*(?=!=|!~|>=|<=|[=~><]|&&|\|\||\)|})/; + textrx = /^(?!?)\s*(`|'|"?)(?\({&\d+}\)|.+?)\2\s*(?=!=|!~|>=|<=|[=~><]|&&|\|\||\)|})/; // TOKEN MARKERS ======================================== const iftm = { rx: ifrx, type: 'if' }, elseiftm = { rx: elseifrx, type: 'elseif' }, @@ -583,7 +583,9 @@ const APILogic = (() => { item.metavalue = true; switch (item.type) { case 'text': - item.groups.argtext = item.groups.argtext.replace(/\$\[\[(\d+)]]/g, ((r, g1) => o.parsedinline[g1].value || 0)); + item.groups.argtext = item.groups.argtext + .replace(/\$\[\[(\d+)]]/g, ((r, g1) => o.parsedinline[g1].value || 0)) + .replace(/\({&(\d+)}\)/, ((r, g1) => o.parsedinline[g1].value || 0)); if (grouplib.hasOwnProperty(item.groups.argtext)) { if (grouplib[item.groups.argtext]) item.value = true; else { @@ -619,6 +621,8 @@ const APILogic = (() => { let logcolor = 'lightseagreen'; let groupname = ''; let negate = false; + let res; + c.memo = c.hasOwnProperty("memo") ? c.memo : { value: false, next: '||' }; nestlog(`CONDITIONS TEST BEGINS`, c.indent, logcolor, msgstate.logging); let o = c.tokens.reduce((m, v, i) => { if ((!m.value && m.next === '&&') || (m.value && m.next === '||')) { @@ -628,22 +632,24 @@ const APILogic = (() => { nestlog(`==AND-GROUP DETECTED: ${v.name || 'no name'}`, c.indent, logcolor, msgstate.logging); groupname = v.name; negate = v.negate; - v = areConditionsTruthy({ tokens: v.contents, indent: c.indent + 1 }); + res = areConditionsTruthy({ tokens: v.contents, indent: c.indent + 1, memo: { ...m } }); + v.value = res.value; if (groupname) { grouplib[groupname] = v.value; } if (negate) v.value = !v.value; } else { nestlog(`==AND-CONDITION DETECTED: lhs>${v.contents[0]} type>${v.type} rhs>${v.contents[1] || ''}`, c.indent, logcolor, msgstate.logging); - v = resolveCondition(v); + ret = resolveCondition(v); + v.value = ret.value; } nestlog(`==VALUE: ${v.value}`, c.indent, logcolor, msgstate.logging); m.value = m.next === '&&' ? m.value && v.value : m.value || v.value; } - nestlog(`==LOOP END MEMO VALUE: ${m.value}, ${m.next}`, c.indent, logcolor, msgstate.logging); m.next = v.next; + nestlog(`==LOOP END MEMO VALUE: ${m.value}, ${m.next}`, c.indent, logcolor, msgstate.logging); return m; - }, { value: false, next: '||' }); + }, c.memo); nestlog(`CONDITIONS TEST ENDS: Conditions are ${o.value}, ${o.next}`, c.indent, logcolor, msgstate.logging); return o; diff --git a/APILogic/script.json b/APILogic/script.json index 2543d4a4f2..14e4fd5845 100644 --- a/APILogic/script.json +++ b/APILogic/script.json @@ -1,7 +1,7 @@ { "name": "APILogic", "script": "APILogic.js", - "version": "2.0.8", + "version": "2.0.9", "description": "APILogic is a meta-script and part of the Meta-Toolbox. APILogic provides chat input logical constructs for shaping the command line, including IF, ELSEIF, and ELSE. It provides a way to define terms for text-replacment operations, or to name condition sets to re-use later in the logical processing. \r\rFor more information, see the original API forum thread:\r\r[APILogic Forum Thread](https://app.roll20.net/forum/post/9771314/script-apilogic-gives-if-slash-elseif-slash-else-processing-to-other-scripts/)\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", "roll20userid": "5962076", @@ -29,6 +29,7 @@ "2.0.4", "2.0.5", "2.0.6", - "2.0.7" + "2.0.7", + "2.0.8" ] } diff --git a/MetaScriptToolbox/0.0.2/MetaScriptToolbox.js b/MetaScriptToolbox/0.0.2/MetaScriptToolbox.js new file mode 100644 index 0000000000..eb4b1b70ed --- /dev/null +++ b/MetaScriptToolbox/0.0.2/MetaScriptToolbox.js @@ -0,0 +1,234 @@ +/* +========================================================= +Name : MetaScriptToolbox +GitHub : +Roll20 Contact : timmaugh +Version : 0.0.2 +Last Update : 6 SEP 2024 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.MetaScriptToolbox = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.MetaScriptToolbox.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } + +const MetaScriptToolbox = (() => { // eslint-disable-line no-unused-vars + const apiproject = 'MetaScriptToolbox'; + const version = '0.0.2'; + const schemaVersion = 0.1; + API_Meta[apiproject].version = version; + const vd = new Date(1725630209434); + 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; + }; + 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: {}, + defaults: {}, + version: schemaVersion + } + break; + } + } + }; + let stateReady = false; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + + 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; + }; + + on('ready', () => { + versionInfo(); + assureState(); + logsig(); + let reqs = [ + { + name: 'ZeroFrame', + version: `1.2.2`, + mod: typeof ZeroFrame !== 'undefined' ? ZeroFrame : undefined, + checks: [['RegisterMetaOp', 'function']], + }, + { + name: 'APILogic', + version: `2.0.9`, + mod: typeof APILogic !== 'undefined' ? APILogic : undefined, + checks: [], + }, + { + name: 'Fetch', + version: `2.1.1`, + mod: typeof Fetch !== 'undefined' ? Fetch : undefined, + checks: [], + }, + { + name: 'SelectManager', + version: `1.1.8`, + mod: typeof SelectManager !== 'undefined' ? SelectManager : undefined, + checks: [['GetPlayerID', 'function'], ['GetSelected', 'function'], ['GetWho', 'function']], + }, + { + name: 'Muler', + version: `2.0.2`, + mod: typeof Muler !== 'undefined' ? Muler : undefined, + checks: [], + }, + { + name: 'Plugger', + version: `1.0.9`, + mod: typeof Plugger !== 'undefined' ? Plugger : undefined, + checks: [], + }, + { + name: 'MathOps', + version: `1.0.8`, + mod: typeof MathOps !== 'undefined' ? MathOps : undefined, + checks: [['MathProcessor', 'function']], + }, + { + name: 'libTokenMarkers', + version: `0.1.2`, + mod: typeof libTokenMarkers !== 'undefined' ? libTokenMarkers : undefined, + checks: [['getStatus', 'function'], ['getStatuses', 'function'], ['getOrderedList', 'function']] + }, + { + name: 'libTable', + version: `1.0.0`, + mod: typeof libTable !== 'undefined' ? libTable : undefined, + checks: [ + ['getTable', 'function'], + ['getTables', 'function'], + ['getItems', 'function'], + ['getItemsByIndex', 'function'], + ['getItemsByName', 'function'], + ['getItemsByWeight', 'function'], + ['getItemsByWeightedIndex', 'function'] + ] + }, + { + name: 'checkLightLevel', + // version: `1.0.0.b3`, + mod: typeof checkLightLevel !== 'undefined' ? checkLightLevel : undefined, + checks: [['isLitBy', 'function']], + optional: true + }, + { + name: 'Messenger', + version: `1.0.1`, + mod: typeof Messenger !== 'undefined' ? Messenger : undefined, + checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function'], ['Css', 'function']] + } + ]; + if (!checkDependencies(reqs)) return; + }); + return {}; +})(); + +{ try { throw new Error(''); } catch (e) { API_Meta.MetaScriptToolbox.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.MetaScriptToolbox.offset); } } diff --git a/MetaScriptToolbox/MetaScriptToolbox.js b/MetaScriptToolbox/MetaScriptToolbox.js index 162a0aa437..eb4b1b70ed 100644 --- a/MetaScriptToolbox/MetaScriptToolbox.js +++ b/MetaScriptToolbox/MetaScriptToolbox.js @@ -3,8 +3,8 @@ Name : MetaScriptToolbox GitHub : Roll20 Contact : timmaugh -Version : 0.0.1 -Last Update : 8/8/2023 +Version : 0.0.2 +Last Update : 6 SEP 2024 ========================================================= */ var API_Meta = API_Meta || {}; @@ -13,10 +13,10 @@ API_Meta.MetaScriptToolbox = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; const MetaScriptToolbox = (() => { // eslint-disable-line no-unused-vars const apiproject = 'MetaScriptToolbox'; - const version = '0.0.1'; + const version = '0.0.2'; const schemaVersion = 0.1; API_Meta[apiproject].version = version; - const vd = new Date(1655476169424); + const vd = new Date(1725630209434); 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}`); }; @@ -152,37 +152,37 @@ const MetaScriptToolbox = (() => { // eslint-disable-line no-unused-vars let reqs = [ { name: 'ZeroFrame', - version: `1.1.6`, + version: `1.2.2`, mod: typeof ZeroFrame !== 'undefined' ? ZeroFrame : undefined, checks: [['RegisterMetaOp', 'function']], }, { name: 'APILogic', - version: `2.0.8`, + version: `2.0.9`, mod: typeof APILogic !== 'undefined' ? APILogic : undefined, checks: [], }, { name: 'Fetch', - version: `2.0.9`, + version: `2.1.1`, mod: typeof Fetch !== 'undefined' ? Fetch : undefined, checks: [], }, { name: 'SelectManager', - version: `1.1.0`, + version: `1.1.8`, mod: typeof SelectManager !== 'undefined' ? SelectManager : undefined, checks: [['GetPlayerID', 'function'], ['GetSelected', 'function'], ['GetWho', 'function']], }, { name: 'Muler', - version: `2.0.1`, + version: `2.0.2`, mod: typeof Muler !== 'undefined' ? Muler : undefined, checks: [], }, { name: 'Plugger', - version: `1.0.6`, + version: `1.0.9`, mod: typeof Plugger !== 'undefined' ? Plugger : undefined, checks: [], }, @@ -221,7 +221,7 @@ const MetaScriptToolbox = (() => { // eslint-disable-line no-unused-vars }, { name: 'Messenger', - version: `1.0.0`, + version: `1.0.1`, mod: typeof Messenger !== 'undefined' ? Messenger : undefined, checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function'], ['Css', 'function']] } diff --git a/MetaScriptToolbox/script.json b/MetaScriptToolbox/script.json index 3f3c0472c0..689e07edb4 100644 --- a/MetaScriptToolbox/script.json +++ b/MetaScriptToolbox/script.json @@ -1,7 +1,7 @@ { "name": "MetaScriptToolbox", "script": "MetaScriptToolbox.js", - "version": "0.0.1", + "version": "0.0.2", "description": "The MetaScriptToolbox is a script that packages the entire suite of metascripts (and their supporting libraries). It does not have a direct interface, but only serves to make sure you have the latest versions of the included metascripts, as well as notifying you should a new metascript be added to the toolbox.", "authors": "timmaugh", "roll20userid": "5962076", @@ -9,5 +9,7 @@ "useroptions": [], "modifies": {}, "conflicts": [], - "previousversions": [] + "previousversions": [ + "0.0.1" + ] } \ No newline at end of file diff --git a/ZeroFrame/1.2.2/ZeroFrame.js b/ZeroFrame/1.2.2/ZeroFrame.js new file mode 100644 index 0000000000..defc90863d --- /dev/null +++ b/ZeroFrame/1.2.2/ZeroFrame.js @@ -0,0 +1,1348 @@ +/* +========================================================= +Name : ZeroFrame +GitHub : https://github.com/TimRohr22/Cauldron/tree/master/ZeroFrame +Roll20 Contact : timmaugh +Version : 1.2.2 +Last Update : 6 SEP 2024 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.ZeroFrame = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.ZeroFrame.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } + +const ZeroFrame = (() => { //eslint-disable-line no-unused-vars + // ================================================== + // VERSION + // ================================================== + const apiproject = 'ZeroFrame'; + API_Meta[apiproject].version = '1.2.2'; + const schemaVersion = 0.2; + const vd = new Date(1725629995074); + let stateReady = false; + 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: + state[apiproject].config.singlebang = true; + /* break; // intentional dropthrough */ /* falls through */ + case 0.2: + /* break; // intentional dropthrough */ /* falls through */ + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + config: { + looporder: [], + logging: false, + singlebang: true + }, + version: schemaVersion + }; + break; + } + } + }; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + 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}`); + assureState(); + }; + 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; + }; + + // ================================================== + // MESSAGE STORAGE + // ================================================== + 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 preservedMsgObj = {}; + const batchMsgLibrary = {}; // will contain key pairs of UUID:originalMsg + + // ================================================== + // META-OP REGISTRATION + // ================================================== + + const loopFuncs = []; + + class Func { + constructor({ func: func = () => { }, priority: priority = 50, handles: handles = [] }) { + this.name = func.name || handles[0] || 'unknown'; + this.func = func; + this.priority = priority; + this.handles = [func.name, ...handles.filter(h => h !== func.name)] + } + } + + const registerMetaOp = (func, options = { priority: 50, handles: [] }) => { + assureState(); + if (!(func.name || (options.handles && options.handles.length))) { + log(`Functions registered for the loop must bear a name or a handle. The unnamed function attempted to register after ${Object.keys(loopFuncs).join(', ')}`); + return; + } + let rFunc = new Func({ func, ...options }); + let statefunc; + if (state[apiproject].config.looporder && state[apiproject].config.looporder.length) { + statefunc = state[apiproject].config.looporder.filter(f => f.name === (rFunc.name || rFunc.handles[0]))[0]; + } + if (statefunc) { + rFunc.priority = statefunc.priority || rFunc.priority; + statefunc.handles = [...new Set([...statefunc.handles, ...rFunc.handles])]; + } else { + state[apiproject].config.looporder.push(rFunc); + } + if (!loopFuncs.filter(f => f.name === rFunc.name || f.name === rFunc.handles[0]).length) { + loopFuncs.push(rFunc); + } + }; + const initState = () => { + return { + runloop: true, + loopcount: 0, + logging: state[apiproject].config.logging || false, + looporder: loopFuncs.sort((a, b) => a.priority > b.priority ? 1 : -1), + history: [], + duplicatecount: 0 + } + }; + const trackhistory = (msg, preservedstate, props = {}) => { + preservedstate.history.push({ + action: props.action, + content: msg.content, + notes: props.notes || '', + status: props.status || '' + }); + }; + + // ================================================== + // LOGGING + // ================================================== + const handleLogging = (msg, preservedstate) => { + let logrx = /{\s*&\s*log\s*}/ig; + msg.content = msg.content.replace(logrx, (r => { //eslint-disable-line no-unused-vars + preservedstate.logging = true; + return ''; + })); + }; + // ================================================== + // MESSAGING AND REPORTING + // ================================================== + const getWhisperTo = (who) => who.toLowerCase() === 'api' ? 'gm' : who.replace(/\s\(gm\)$/i, ''); + const HE = (() => { //eslint-disable-line no-unused-vars + const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g, '\\$1'); + const e = (s) => `&${s};`; + const entities = { + '<': e('lt'), + '>': e('gt'), + "'": e('#39'), + '@': e('#64'), + '{': e('#123'), + '|': e('#124'), + '}': e('#125'), + '[': e('#91'), + ']': e('#93'), + '"': e('quot'), + '*': e('#42') + }; + const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`, 'g'); + return (s) => s.replace(re, (c) => (entities[c] || c)); + })(); + const msgframe = `
ZeroFrame
__BODYCONTENT__
 
`; + const msgsimpleframe = `
ZeroFrame
__BODYCONTENT__
 
`; + const msgsimplecontent = `
__CONTENTMESSAGE__
`; + const msgconfigcontent = `
__SCRIPTNAME__
__ALIASES__
`; + // const msgconfigcontent = `
__PRIORITY__
__SCRIPTNAME__
__ALIASES__
`; + const msglogcontent = `
 
__SCRIPTNAME__
__LOGMESSAGE__
`; + + const msgboxfull = ({ c: c = 'chat message', sendas: sas = 'API', wto: wto = '', simple: simple = false }) => { + let msg = (simple ? msgsimpleframe : msgframe).replace("__BODYCONTENT__", c); + if (!['API', ''].includes(wto)) msg = `/w "${wto.replace(' (GM)', '')}" ${msg}`; + sendChat(sas, msg); + }; + const msgbox = ({ c: c = 'chat message', sendas: sas = 'API', wto: wto = '' }) => { + let msg = msgsimplecontent.replace('__CONTENTMESSAGE__', c); + msgboxfull({ c: msg, wto: wto, simple: true, sendas: sas }); + } + const buildLog = (msg, ps, apitrigger) => { + const statuscolor = { + loop: '#ff9637', + changed: '#339b00', + unchanged: '#001ea6', + unresolved: '#b70000', + stop: '#b70000', + simple: '#ff9637', + release: '#001ea6' + } + let rows = ps.history.reduce((m, v) => { + if (/^ORIGINAL/.test(v.action)) return m; + let note = ''; + switch (v.status) { + case 'unchanged': + if (v.notes.length) note = `NOTES: ${v.notes}`; + break; + case 'release': + case 'stop': + case 'simple': + if (v.notes.length) note = `NOTES: ${v.notes}`; + note += note.length ? '
' : ''; + note += `FINAL MESSAGE
${v.content.replace(apitrigger, '').replace(/&{template:/g, `&{template:`)}`; + break; + default: + note = v.content.replace(apitrigger, ''); + if (v.notes.length) note += `
NOTES: ${v.notes}`; + } + // if (v.status !== 'unchanged') note = v.content.replace(apitrigger,''); + // if (note.length && v.notes.length) note += `
NOTES: ${v.notes}`; + return m + msglogcontent + .replace(/__STATUSCOLOR__/g, c => { return statuscolor[v.status] || statuscolor.loop; }) //eslint-disable-line no-unused-vars + .replace('__SCRIPTNAME__', v.action.toUpperCase()) + .replace('__LOGMESSAGE__', note); + }, ''); + msgboxfull({ c: rows, wto: getWhisperTo(msg.who), simple: true }); + + }; + const buildConfig = (msg) => { + let looporder = loopFuncs.sort((a, b) => a.priority > b.priority ? 1 : -1); + let rows = looporder.reduce((m, v) => { + return m + msgconfigcontent + .replace(/__PRIORITY__/g, v.priority) + .replace(/__SCRIPTNAME__/g, v.name) + .replace(/__ALIASES__/g, v.handles.join(', ')) + .replace(/__ALIAS1__/g, v.handles[0]); + }, ''); + + msgboxfull({ c: rows, wto: getWhisperTo(msg.who) }); + + }; + + // ================================================== + // REGEX MANAGEMENT + // ================================================== + const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); }; + const getFirst = (cmd, ...args) => { + // pass in objects of form: {type: 'text', rx: /regex/} + // return object of form : {regex exec object with property 'type': 'text'} + + let ret = {}; + let r; + args.find(a => { + r = a.rx.exec(cmd); + if (r && (!ret.length || r.index < ret.index)) { + ret = Object.assign(r, { type: a.type }); + } + a.lastIndex = 0; + }, ret); + return ret; + }; + const getConfigItem = e => { + return state[apiproject].config[e]; + }; + + // ================================================== + // UTILITIES + // ================================================== + 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'); + }; + + // ================================================== + // ROLL MANAGEMENT + // ================================================== + const nestedInline = (preserved) => { + let ores, + ires, + c = '', + index = 0, + nestedindexarray = [], + nestedlvl = 0, + outeropenrx = /(? 0) nestedindexarray.unshift({ index: index, value: preserved.parsedinline[ires[1] || ires[2]].value, replacestring: ires[0] }); + index += ires[0].length; + } else { + // this would probably indicate an error -- something like $[[NaN]] + index += ores[0].length; + } + break; + case 'outer': + nestedlvl++; + index += ores.index + ores[0].length; + break; + case 'close': + nestedlvl--; + index += ores.index + ores[0].length; + break; + } + } + //since we are working in descending order, all of our indices will survive the replacement operation + nestedindexarray.forEach(r => { + preserved.content = `${preserved.content.slice(0, r.index)}${r.value}${preserved.content.slice(r.index + r.replacestring.length, preserved.content.length)}`; + }); + // return preserved.content; + }; + const getValues = (msg, lastpass = false) => { + // replace inline rolls tagged with .value + const valuerx = /\$\[\[(?\d+)]]\.value/gi; + const value2rx = /\({&(?\d+)}\)\.value/gi; + const itemsrx = /\$\[\[(?\d+)]]\.items\((?(?:'[^']+'|"[^"]+"|`[^`]+`|(?:[^'"`](?:#|\|))(?:'[^']+'|"[^"]+"|`[^`]+`|[^'"`)][^)]*)|[^'"`)][^#\|)]?[^)]*))\)/gi; + const items2rx = /\({&(?\d+)}\)\.items\((?(?:'[^']+'|"[^"]+"|`[^`]+`|(?:[^'"`](?:#|\|))(?:'[^']+'|"[^"]+"|`[^`]+`|[^'"`)][^)]*)|[^'"`)][^#\|)]?[^)]*))\)/gi; + const items3rx = /\$\[\[(?\d+)]]\.items(?=[^(])/gi; + const items4rx = /\({&(?\d+)}\)\.items(?=[^(])/gi; + let retval = false; + [valuerx, value2rx].forEach(rx => { + msg.content = msg.content.replace(rx, ((r, g1) => { + retval = true; + if (msg.inlinerolls.length > g1) { + return msg.parsedinline[g1].value; + } else if (lastpass) { + return '0'; + } else { + return r; + } + })); + }); + [itemsrx, items2rx, items3rx, items4rx].forEach((rx,rxIndex) => { + msg.content = msg.content.replace(rx, ((r, g1, separator) => { + let delim = ','; + let res; + if (msg.inlinerolls.length > g1) { + retval = true; + if (rxIndex < 2) { + if (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(separator)) { // enclosed in quotation marks of some sort + delim = separator.slice(1, -1); + } else if (/^(?[^'"`])(?:#|\|)(?.+)$/.test(separator)) { // deferral character is present + res = /^(?[^'"`])(?:#|\|)(?.+)$/.exec(separator); + delim = (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(res.groups.deferredtext) + ? res.groups.deferredtext.slice(1, -1) // enclosed in quotation marks of some sort + : res.groups.deferredtext) // not enclosed in quotation marks + .replace(new RegExp(escapeRegExp(res.groups.deferral), 'g'), '') + } else { + delim = separator; + } + } + return msg.parsedinline[g1].getTableValues().length + ? msg.parsedinline[g1].getTableValues().join(delim) + : msg.parsedinline[g1].value; + } else if (lastpass) { + retval = true; + return '0'; + } else { + return r; + } + })); + + }); + [items3rx, items4rx].forEach(rx => { + msg.content = msg.content.replace(rx, ((r, g1) => { + let delim = ', '; + let res; + if (msg.inlinerolls.length > g1) { + retval = true; + return msg.parsedinline[g1].getTableValues().join(delim); + } else if (lastpass) { + retval = true; + return '0'; + } else { + return r; + } + })); + + }); + + //msg.content = msg.content.replace(valuerx, ((r, g1) => { + // retval = true; + // if (msg.inlinerolls.length > g1) { + // return msg.parsedinline[g1].value; + // } else if (lastpass) { + // return '0'; + // } else { + // return r; + // } + //})); + //msg.content = msg.content.replace(value2rx, ((r, g1) => { + // if (msg.inlinerolls.length > g1) { + // retval = true; + // return msg.parsedinline[g1].value; + // } else if (lastpass) { + // retval = true; + // return '0'; + // } else { + // return r; + // } + //})); + //msg.content = msg.content.replace(itemsrx, ((r, g1, separator) => { + // let delim = ','; + // let res; + // if (msg.inlinerolls.length > g1) { + // retval = true; + // if (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(separator)) { // enclosed in quotation marks of some sort + // delim = separator.slice(1, -1); + // } else if (/^(?[^'"`])(?:#|\|)(?.+)$/.test(separator)) { // deferral character is present + // res = /^(?[^'"`])(?:#|\|)(?.+)$/.exec(separator); + // delim = (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(res.groups.deferredtext) + // ? res.groups.deferredtext.slice(1, -1) // enclosed in quotation marks of some sort + // : res.groups.deferredtext) // not enclosed in quotation marks + // .replace(new RegExp(escapeRegExp(res.groups.deferral),'g'),'') + // } else { + // delim = separator; + // } + // return msg.parsedinline[g1].getTableValues().join(delim); + // } else if (lastpass) { + // retval = true; + // return '0'; + // } else { + // return r; + // } + //})); + //msg.content = msg.content.replace(items2rx, ((r, g1, separator) => { + // let delim = ','; + // let res; + // if (msg.inlinerolls.length > g1) { + // retval = true; + // if (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(separator)) { // enclosed in quotation marks of some sort + // delim = separator.slice(1, -1); + // } else if (/^(?[^'"`])(?:#|\|)(?.+)$/.test(separator)) { // deferral character is present + // res = /^(?[^'"`])(?:#|\|)(?.+)$/.exec(separator); + // delim = (/^(?:'[^']+'|"[^"]+"|`[^`]+`)$/.test(res.groups.deferredtext) + // ? res.groups.deferredtext.slice(1, -1) // enclosed in quotation marks of some sort + // : res.groups.deferredtext) // not enclosed in quotation marks + // .replace(new RegExp(escapeRegExp(res.groups.deferral), 'g'), '') + // } else { + // delim = separator; + // } + // return msg.parsedinline[g1].getTableValues().join(delim); + // } else if (lastpass) { + // retval = true; + // return '0'; + // } else { + // return r; + // } + //})); + return retval; + }; + const getLoopRolls = (msg, preserved, preservedstate) => { + let replaceTrack = {}; + if (msg.inlinerolls) { + // insert inline rolls to preserved message, correct the placeholder shorthand index + msg.inlinerolls.forEach((r, i) => { + preserved.inlinerolls.push(r); + replaceTrack[i] = (preserved.inlinerolls.length - 1); + }); + Object.keys(replaceTrack).reverse().forEach(k => { + msg.content = msg.content.replace(new RegExp(`\\$\\[\\[(${k})]]`, 'g'), `$[[${replaceTrack[k]}]]`); + }); + preserved.parsedinline = [...(preserved.parsedinline || []), ...libInline.getRollData(msg)]; + preservedstate.runloop = true; + } + }; + const handleInit = (msg, preserved, preservedstate) => { + if (!preserved.initArray || !preserved.initArray.length) { return; } + class TrackerEntry { + constructor(id,val,pgid,custom='') { + this.id = id; + this.pr = `${val}`; + this.custom = custom; + this._pageid = pgid; + } + } + let parsed = libInline.getRollData(msg); + let to = JSON.parse(Campaign().get('turnorder')); + + preserved.initArray.forEach(e => { + if (!e[1].token || !e[1].token.id || parsed.length <= e[0]) { return; } + let newentry = new TrackerEntry(e[1].token.id, parsed[e[0]].value, e[1].token.get('pageid')); + let oldentry; + switch (e[1].type) { + case 'new': + to.push(newentry); + break; + case 'value': + oldentry = to.filter(te => te.id === e[1].token.id && te.pr === e[1].value)[0]; + if (oldentry && oldentry.id) { + Object.assign(oldentry, newentry); + } else { + to.push(newentry); + } + break; + case 'index': + oldentry = to.filter(te => te.id === e[1].token.id)[Math.max(0,parseInt(e[1].value)-1)]; + if (oldentry && oldentry.id) { + Object.assign(oldentry, newentry); + } else { + to.push(newentry); + } + break; + default: + oldentry = to.filter(te => te.id === e[1].token.id)[0]; + if (oldentry && oldentry.id) { + Object.assign(oldentry, newentry); + } else { + to.push(newentry); + } + } + }); + Campaign().set({ turnorder: JSON.stringify(to) }); + delete preserved.initArray; + }; + const customTracker = (preserved) => { + let res, + index = 0, + rollcount = 0, + trackerindexarray = [], + initArray = [], + inlineopenrx = /(? new RegExp(`^${rx.source}`, rx.flags); + + const getToken = (info, fromto = {}) => { + const checkControl = (t) => { + if (playerIsGM(preserved.playerid)) { return true; } + let cby = t.get('represents').length + ? findObjs({ id: t.get('represents') })[0].get('controlledby') + : t.get('controlledby'); + return cby.split(',').reduce((m, p) => { + return m || p === 'all' || p === preserved.playerid; + }, false); + }; + let pgid = getPageForPlayer(preserved.playerid); + let tokens = []; + if (fromto && fromto.type) { // from turn order, only + tokens = JSON.parse(Campaign().get('turnorder')) + .filter(e => playerIsGM(preserved.playerid) || e._pageid === pgid) + .map(e => { + e.token = getObj('graphic', e.id); + return e; + }) + .filter(e => checkControl(e.token)) + .filter(e => { + if (e.token.id === info || e.token.get('name') === info) { return true; } + let c = (findObjs({ type: 'character', id: e.token.get('represents') })[0] || { get: () => { return undefined; } }); + return c.id === info || c.get('name') === info; + }) + .filter((e, i) => { + if (fromto.type === 'value') { + return e.pr === fromto.value; + } else if (fromto.type === 'index') { + return i === parseInt(fromto.value)-1; + } + return true; + }) + .map(e => e.token); + if (!tokens.length) { + fromto.type = 'new'; + } + } + if (!tokens.length) { + tokens = [ + ...findObjs({ type: 'graphic', subtype: 'token', id: info }), + ...findObjs({ type: 'graphic', subtype: 'card', id: info }), + ...findObjs({ type: 'graphic', subtype: 'token', name: info, pageid: pgid }), + ...findObjs({ type: 'graphic', subtype: 'token', pageid: pgid }) + .filter(t => t.get('represents').length && findObjs({ type: 'character', id: t.get('represents') })[0].get('name') === info) + ].filter(checkControl); + if (!tokens.length) { + tokens = findObjs({ type: 'graphic', subtype: 'token', name: info }); + if (tokens.length > 1 || !checkControl(tokens[0])) { + tokens = []; + } + } + } + return tokens[0]; + }; + + const getTrackerRecord = (query) => { + let partsrx = /^([^@|+|#]+){0,1}(@(.+)|#(\d+)|\+){0,1}/; + let res = partsrx.exec(query); + let ret = {}; + if (res[2] === '+') { // add new turn + ret.type = 'new'; + } else if (res[3]) { // current tracker value + ret.type = 'value'; + ret.value = res[3]; + } else if (res[4]) { // index of turn for multi-turn token + ret.type = 'index'; + ret.value = res[4]; + } + if (!res[1]) { // no token identifier + if (preserved.selected && preserved.selected.length) { + ret.token = getToken(preserved.selected[0]._id); + } + } else { + if (ret.type && ['value', 'index'].includes(ret.type)) { + ret.token = getToken(res[1], ret); + } else { + ret.token = getToken(res[1]); + } + } + return ret; + }; + const openRoll = (index) => { + let testSet = [trackertm, opentm, closetm, eostm]; + let res; + let tokens = []; + let tags = []; + + res = getFirst(preserved.content.slice(index), ...testSet); + index += res.index; + while (!['eos', 'close'].includes(res.type)) { + if (res.type === 'open') { + index += res[0].length; + index = openRoll(index); + } else if (res.type === 'tracker') { + tags.push(index); + if (!res[1]) { + if (preserved.selected && preserved.selected.length) { + tokens.push(getTrackerRecord(preserved.selected[0]._id)); + } + } else { + res[1].split(/\s*,\s*/).forEach(t => tokens.push(getTrackerRecord(t))); + } + index += res[0].length; + } + res = getFirst(preserved.content.slice(index), ...testSet); + index += res.index; + } + + if (res.type === 'close') { + trackerindexarray.push(...tags); + initArray.push(...tokens.map(t => [rollcount, t])); + rollcount++; + index += res[0].length; + } + return index; + }; + + while (index < preserved.content.length) { + res = getFirst(preserved.content.slice(index), opentm, eostm); + index += res.index; + if (res.type === 'open') { + index += res[0].length; + index = openRoll(index); + } else { + index = preserved.content.length; + } + } + + trackerindexarray.sort((a, b) => b - a).forEach(t => { + preserved.content = `${preserved.content.slice(0, t)}${preserved.content.slice(t).replace(assertStart(trackerrx), '')}`; + }); + preserved.initArray = initArray; + }; + + // ================================================== + // GLOBAL DEFINITIONS + // ================================================== + + const getGlobals = msg => { + + class TextToken { + constructor({ value: value = '' } = {}) { + this.type = 'text'; + this.value = value; + } + } + class GlobalToken { + constructor({ value: value = '' } = {}) { + this.type = 'global'; + this.value = value; + } + } + let index = 0; + let gres; + let globalrx = /{&\s*globals?\s+/gi; + // let definitionrx = /\(\s*\[\s*(?.+?)\s*]\s*('|"|`?)(?.*?)\2\)\s*/g; + let definitionrx = /\(\s*\[\s*(?.+?)\s*]\s*('|"|`?)(?.*?)\2(?:\)(? { + let pos = 0; + let loop = true; + while (loop && pos <= c.length - 1) { + if (c.charAt(pos) === '{') counter++; + else if (c.charAt(pos) === '}') counter--; + if (counter === 0) loop = false; + pos++; + } + return loop ? undefined : pos; + } + while (globalrx.test(msg.content)) { + globalrx.lastIndex = index; + gres = globalrx.exec(msg.content); + tokens.push(new TextToken({ value: msg.content.slice(index, gres.index) })); + let p = closureCheck(msg.content.slice(gres.index)) || gres[0].length; + tokens.push(new GlobalToken({ value: msg.content.slice(gres.index, gres.index + p) })); + index = gres.index + p; + } + tokens.push(new TextToken({ value: msg.content.slice(index) })); + definitionrx.lastIndex = 0; + return tokens.reduce((m, t) => { + if (t.type === 'text' || (t.type === 'global' && !/}$/.test(t.value))) { + m.cmd = `${m.cmd}${t.value}`; + } else { + t.value.replace(definitionrx, (match, term, _, def) => { + m.globals[term] = def; + return match; + }); + } + return m; + }, { cmd: '', globals: {} }); + + }; + // ================================================== + // THE LOOP & LOOP MANAGEMENT + // ================================================== + const setOrder = (msg, preservedstate) => { + let orderrx = /(\()?{&\s*0\s+([^}]+?)\s*}((?<=\({&\s*0\s+([^}]+?)\s*})\)|\1)/g; + msg.content = msg.content.replace(orderrx, (m, padding, list) => { + let order = list + .split(/\s+/) + .map(l => preservedstate.looporder.filter(f => f.name === l || f.handles.includes(l))[0]) + .filter(f => f); + let orderedfuncs = order.map(f => f.name); + preservedstate.looporder = [...order, ...preservedstate.looporder.filter(f => !orderedfuncs.includes(f.name))]; + return ''; + }) + }; + const runLoop = (preserved, preservedstate, apitrigger, msg = {}) => { + const delayrx = /{&\s*delay(?:\((.+?)\))?\s+(.*?)\s*}/gi + preservedstate.runloop = false; + preservedstate.loopcount++; + trackhistory(msg, preservedstate, { action: `LOOP ${preservedstate.loopcount}` }); + handleLogging(msg, preservedstate); + setOrder(msg, preservedstate); + if (preservedstate.logging) { + log(`LOOP ${preservedstate.loopcount}`); + } + if (preservedstate.logging) { + log(`====MSG DATA====`); + log(` CONT: ${preserved.content}`); + log(` DEFS: ${JSON.stringify(preserved.definitions || [])}`); + } + handleInit(msg, preserved, preservedstate); + getLoopRolls(msg, preserved, preservedstate); + preserved.content = msg.content.replace(/()?\n/g, '({&br})'); + if (!preserved.rolltemplate && msg.rolltemplate && msg.rolltemplate.length) preserved.rolltemplate = msg.rolltemplate; + msg.content = `${msg.apitrigger}`; + // manage delay + let delay = 0; + let delaydeferrals = []; + preserved.content = preserved.content.replace(delayrx, (m, def, del) => { + delay = Math.max(delay, (Number(del) || 0)); + if (def) delaydeferrals.push(def); + return ''; + }); + if (delay > 0) { + let delaycmd = delaydeferrals.reduce((m, def) => { + m = m.replace(new RegExp(escapeRegExp(def), 'g'), ''); + return m; + }, preserved.content); + setTimeout(sendChat, delay * 1000, '', delaycmd); + msg.content = ''; // flatten the original message so other scripts don't take action + return { delay: true }; + } + preservedstate.runloop = getValues(preserved) || preservedstate.runloop; + // manage global definitions + let globalCheck = getGlobals(preserved); + let globalnote = 'No global detected.'; + if (Object.keys(globalCheck.globals).length) { + globalnote = Object.keys(globalCheck.globals).map(k => `• ${k}: ${globalCheck.globals[k]}`).join('
'); + } + preserved.globals = Object.assign({}, (preserved.globals || {}), globalCheck.globals); + Object.keys(preserved.globals).forEach(k => { + globalCheck.cmd = globalCheck.cmd.replace(new RegExp(escapeRegExp(k), 'g'), preserved.globals[k]); + }); + if (globalCheck.cmd !== preserved.content) { + preserved.content = globalCheck.cmd; + trackhistory(preserved, preservedstate, { action: 'GLOBALS', notes: `Global tag detected.
${globalnote}`, status: 'changed' }); + preservedstate.runloop = true; + } else { + trackhistory(preserved, preservedstate, { action: 'GLOBALS', notes: ``, status: 'unchanged' }); + } + + // loop through registered functions + let funcret; + preservedstate.looporder.forEach(f => { + if (preservedstate.logging) log(`...RUNNING ${f.name}`); + + funcret = f.func(preserved, preservedstate); + if (preservedstate.logging) { + log(`....MSG DATA....`); + log(` CONT: ${preserved.content}`); + log(` DEFS: ${JSON.stringify(preserved.definitions || [])}`); + } + // returned object should include { runloop: boolean, status: (changed|unchanged|unresolved), notes: text} + trackhistory(preserved, preservedstate, { action: f.name, notes: funcret.notes, status: funcret.status }); + preservedstate.runloop = preservedstate.runloop || funcret.runloop; + // replace inline rolls tagged with .value + getValues(preserved); + + }); + // custom roll marker (open/close) + preserved.content = preserved.content.replace(/(\({&\s*r\s*}\)|{&\s*r\s*})/gim, m => { + preservedstate.runloop = true; + return '[['; + }); + preserved.content = preserved.content.replace(/(\({&\s*\/r\s*}\)|{&\s*\/r\s*})/gim, m => { + preservedstate.runloop = true; + return ']]' + }); + + // see if we're done + if (preservedstate.runloop) { + if (preservedstate.history.filter(h => /^LOOP\s/.test(h.action) && h.content === preserved.content).length > 5) { + msgbox({ c: 'Possible infinite loop detected. Check ZeroFrame log for more information.', wto: preserved.who }); + preservedstate.logging = true; + releaseMsg(preserved, preservedstate, apitrigger, msg); + } else { + // un-escape characters + preserved.content = preserved.content.replace(/(\[\\+]|\\.)/gm, m => { + if (/^\[/.test(m)) { + return m.length === 3 ? `[` : `[${Array(m.length - 2).join(`\\`)}]`; + } else { + return `${Array(m.length - 1).join(`\\`)}${m.slice(-1)}`; + } + }); + // custom roll marker (open/close) + preserved.content = preserved.content.replace(/(\({&\s*r\s*}\)|{&\s*r\s*})/gim, '[['); + preserved.content = preserved.content.replace(/(\({&\s*\/r\s*}\)|{&\s*\/r\s*})/gim, ']]'); + // convert nested inline rolls to value + nestedInline(preserved); + // look for {&tracker} tags + customTracker(preserved); + // replace other inline roll markers with ({&#}) formation + preserved.content = preserved.content.replace(/\$\[\[(\d+)]]/g, `({&$1})`); + // properly format rolls that would normally fail in the API (but work in chat) + preserved.content = preserved.content.replace(/\[\[\s+/g, '[['); + // send new command line through chat + sendChat('', preserved.content); + msg.content = ''; // flatten the original message so other scripts don't take action + } + } else { + return releaseMsg(preserved, preservedstate, apitrigger, msg); + } + }; + + // ================================================== + // RELEASING THE MESSAGE + // ================================================== + const releaseMsg = (preserved, preservedstate, apitrigger, msg) => { + // we're on our way out of the script, format everything and release message + let notes = []; + let releaseAction = `OUTRO`; + // remove the apitrigger + preserved.content = preserved.content.replace(apitrigger, ''); + // replace all ZF formatted inline roll shorthand markers with roll20 formatted shorthand markers + preserved.content = preserved.content.replace(/\({&(\d+)}\)/g, `$[[$1]]`); + // replace inline rolls tagged with .value + getValues(preserved, true); + + const stoprx = /(\()?{&\s*stop\s*}((?<=\({&\s*stop\s*})\)|\1)/gi, + escaperx = /(\()?{&\s*escape\s+([^}]+?)\s*}((?<=\({&\s*escape\s+([^}]+?)\s*})\)|\1)/gi, + simplerx = /(\()?{&\s*(simple|flat)\s*}((?<=\({&\s*(simple|flat)\s*})\)|\1)/gi, + templaterx = /(\()?{&\s*template:([^}]+?)}((?<=\({&\s*template:([^}]+?)})\)|\1)/gi; + + const escapeCheck = () => { + // check for ESCAPE tag + let escapearray = []; + if (preserved.content.match(escaperx)) { + notes.push(`ESCAPE tag detected`) + preserved.content = preserved.content.replace(escaperx, (m, padding, escchar) => { + escapearray.push(escchar); + return ``; + }); + escapearray.forEach(e => { + preserved.content = preserved.content.replace(new RegExp(escapeRegExp(e), 'g'), ''); + }); + } + }; + // check for STOP tag + if (preserved.content.match(stoprx)) { + trackhistory(preserved, preservedstate, { action: releaseAction, notes: `STOP detected`, status: 'stop' }); + if (preservedstate.logging) buildLog(preserved, preservedstate, apitrigger); + preserved.content = ''; + return { release: true }; + } + // check for TEMPLATE tag + let temptag; + if (preserved.content.match(templaterx)) { + preserved.content = preserved.content.replace(templaterx, (m, padding, template) => { + temptag = true; + notes.push(`TEMPLATE tag detected`); + return `&{template:${template}}`; + }); + } + // line break replacements + preserved.content = preserved.content + .replace(/(\({&\s*cr\s*}\)|{&\s*cr\s*})/gi, '
\n') + .replace(/(\({&\s*nl\s*}\)|{&\s*nl\s*})/gi, '\n') + .replace(/(\({&\s*tp\s*}\)|{&\s*tp\s*})/gi, '{{') + .replace(/(\({&\s*\/tp\s*}\)|{&\s*\/tp\s*})/gi, '}}'); + // check for SIMPLE tag + if (preserved.content.match(simplerx)) { + notes.push(`SIMPLE or FLAT tag detected`) + preserved.content = preserved.content.replace(/^!+\s*/, '') + .replace(simplerx, '') + .replace(/\$\[\[(\d+)]]/g, ((m, g1) => typeof preserved.parsedinline[g1] === 'undefined' ? m : preserved.parsedinline[g1].getRollTip())) + .replace(/\({&br}\)/g, '
\n'); + if (preserved.rolltemplate && !temptag) { + let dbpos = preserved.content.indexOf(`{{`); + dbpos = dbpos === -1 ? 0 : dbpos; + preserved.content = `${preserved.content.slice(0, dbpos)}&{template:${preserved.rolltemplate}} ${preserved.content.slice(dbpos)}`; + } + let speakas = ''; + if (preserved.who.toLowerCase() === 'api') { + speakas = ''; + } else { + speakas = (findObjs({ type: 'character' }).filter(c => c.get('name') === preserved.who)[0] || { id: '' }).id; + if (speakas) speakas = `character|${speakas}`; + else speakas = `player|${preserved.playerid}`; + } + trackhistory(preserved, preservedstate, { action: releaseAction, notes: notes.join('
'), status: 'simple' }); + if (preservedstate.logging) buildLog(preserved, preservedstate, apitrigger); + escapeCheck(); + sendChat(speakas, preserved.content); + setTimeout(() => { delete preservedMsgObj[apitrigger] }, 3000); + return { release: true }; + } else if (getConfigItem('singlebang')) { + preserved.content = preserved.content.replace(/^!!+\s*/, '!'); + } + escapeCheck(); + trackhistory(preserved, preservedstate, { action: releaseAction, notes: notes.join('
'), status: 'release' }); + if (preservedstate.logging) buildLog(preserved, preservedstate, apitrigger); + + // release the message to other scripts (FINAL OUTPUT) + preserved.content = preserved.content.replace(/\({&br}\)/g, '
\n'); + if (preserved.inlinerolls && !preserved.inlinerolls.length) delete preserved.inlinerolls; + Object.keys(preserved).forEach(k => msg[k] = preserved[k]); + + setTimeout(() => { delete preservedMsgObj[apitrigger] }, 3000); + return { release: true }; + }; + const zfconfig = /^!0\s*(?(?:(?:[A-Za-z]+\|\d+)(?:\s+|$))+)/; + const testConstructs = (c) => { + if (/^!0(\s+(cfg|config)|\s*$)/.test(c)) return 'showconfig'; + if (zfconfig.test(c)) return 'runconfig'; + if (/^!0(\s+help|$)/.test(c)) return 'help'; + }; + + // ================================================== + // HANDLE INPUT + // ================================================== + const handleInput = (msg) => { + const trigrx = new RegExp(`^!(${Object.keys(preservedMsgObj).join('|')})`); + const batchtrigrx = new RegExp(`^!(${Object.keys(batchMsgLibrary).map(k => escapeRegExp(`{&batch ${k}}`)).join('|')})`, ''); + let preserved, + preservedstate, + apitrigger; // the apitrigger used by the message + let restoreMsg; + if (msg.type !== 'api') return; + let configtest = testConstructs(msg.content); // special commands for zeroframe + if (configtest) { + let statefunc, + localfunc; + let configerrors = []; + switch (configtest) { + case 'showconfig': + buildConfig(msg); + break; + case 'runconfig': + zfconfig.exec(msg.content).groups.scripts + .trim() + .split(/\s+/) + .map(c => c.split('|')) + .forEach(c => { + statefunc = state[apiproject].config.looporder.filter(f => f.name === c[0] || f.handles.includes(c[0]))[0]; + if (!statefunc) { + configerrors.push(`No script found for ${c[0]}.`); + } else { + if (isNaN(Number(c[1]))) { + configerrors.push(`Priority supplied for ${c[0]} was not a number.`); + } else { + if (statefunc) statefunc.priority = Number(c[1]); + localfunc = loopFuncs.filter(f => f.name === c[0] || f.handles.includes(c[0]))[0]; + if (localfunc) localfunc.priority = Number(c[1]); + } + } + }); + buildConfig(msg); + if (configerrors.length) { + msgbox({ c: configerrors.join('
'), wto: msg.who }); + } + break; + case 'help': + // TO DO: build help output + break; + default: + } + } else { + const skiprx = /(\()?{&\s*skip\s*}((?<=\({&\s*skip\s*})\)|\1)/gi; + if (msg.content.match(skiprx)) { + msg.content = msg.content.replace(skiprx, ''); + return; + } + if (Object.keys(preservedMsgObj).length && trigrx.test(msg.content)) { // check all active apitriggers in play + apitrigger = trigrx.exec(msg.content)[1]; + preserved = preservedMsgObj[apitrigger].message; + preservedstate = preservedMsgObj[apitrigger].state; + } else { // not prepended with apitrigger, original or batch-dispatched message + if (Object.keys(batchMsgLibrary).length && batchtrigrx.test(msg.content)) { + let bres = batchtrigrx.exec(msg.content); + let msgID = bres[0].slice(9, -1); + msg.content = `!${msg.content.slice(bres[0].length)}`; + restoreMsg = batchMsgLibrary[msgID]; + if (restoreMsg) { + msg.batch = msgID; + } + } + msg.unlock = { zeroframe: generateUUID() }; + apitrigger = `${apiproject}${generateUUID()}`; + msg.apitrigger = apitrigger; + msg.origcontent = msg.content; + msg.content = msg.content.replace(/()?\n/g, '({&br})'); //.replace(/^!(\{\{(.*)\}\})/, '!$2'); + msg.content = `!${apitrigger}${msg.content.slice(1)}`; + if (restoreMsg && restoreMsg.hasOwnProperty('message')) { + // this is a batched dispatch, restore non-Roll20 properties like mules, conditional tests, definitions, etc. + Object.keys(restoreMsg.message).filter(k => !['inlinerolls', 'parsedinline', 'content'].includes(k)) + .forEach(k => msg[k] = msg[k] || restoreMsg.message[k]); + } + preservedMsgObj[apitrigger] = { message: _.clone(msg), state: initState() }; + preserved = preservedMsgObj[apitrigger].message; + preservedstate = preservedMsgObj[apitrigger].state; + + if (restoreMsg && restoreMsg.hasOwnProperty('message') && restoreMsg.message.hasOwnProperty('inlinerolls') && restoreMsg.message.inlinerolls.length) { + preserved.inlinerolls = [...restoreMsg.message.inlinerolls]; + preserved.parsedinline = [...restoreMsg.message.parsedinline]; + } else { + preserved.inlinerolls = []; + preserved.parsedinline = []; + } + + trackhistory(preserved, preservedstate, { action: 'ORIGINAL MESSAGE' }); + } + let loopstate = runLoop(preserved, preservedstate, apitrigger, msg); + if (loopstate && loopstate.delay) { //if we delay the command, we should not immediately dispatch the next + return; + } + if (loopstate && loopstate.release && preserved.batch) { + restoreMsg = restoreMsg || batchMsgLibrary[preserved.batch]; + if (restoreMsg && restoreMsg.hasOwnProperty('commands')) { + if (restoreMsg.commands.length) { + sendChat('BatchOp', restoreMsg.commands.shift()); + } else { + delete batchMsgLibrary[restoreMsg.message.messageID]; + } + } + } + } + }; + + // ================================================== + // BATCH OPERATIONS + // ================================================== + const getBatchTextBreakpoint = c => { + let counter = 1; + let pos = 3; + let openprime = false; + let closeprime = false; + while (counter !== 0 && pos <= c.length - 1) { + if (c.charAt(pos) === '{') { + closeprime = false; + if (openprime) { + counter++; + openprime = false; + } else { + openprime = true; + } + } else if (c.charAt(pos) === '}') { + openprime = false; + if (closeprime) { + counter--; + closeprime = false; + } else { + closeprime = true; + } + } else { + openprime = false; + closeprime = false; + } + pos++; + } + return pos; + }; + + // ================================================== + // BATCH HANDLE INPUT + // ================================================== + const handleBatchInput = (msg) => { + if (msg.type !== 'api' || !/^!{{/.test(msg.content)) return; + //Object.keys(batchMsgLibrary).filter(k => Date.now() - batchMsgLibrary[k].time > 10000).forEach(k => delete batchMsgLibrary[k]); + msg.messageID = undefined; + + const storeOutbound = (cmd) => { + if (!msg.messageID) { + msg.messageID = generateUUID(); + batchMsgLibrary[msg.messageID] = { message: _.clone(msg), time: Date.now(), commands: [] }; + } + batchMsgLibrary[msg.messageID].commands.push(`!{&batch ${msg.messageID}}${cmd.replace(/\$\[\[(\d+)]]/g, `({&$1})`)}`); + }; + let cleancmd = msg.content.replace(/\({\)/g, '{{').replace(/\(}\)/g, '}}'); + let breakpoint = getBatchTextBreakpoint(cleancmd) + 1; + let [batchText, remainingText] = [cleancmd.slice(0, breakpoint), cleancmd.slice(breakpoint)]; + let lines = batchText.split(/()?\n/gi) + .map(l => (l || '').trim()) + .filter(l => l.length && '
' !== l) + .reduce((m, l, i, a) => { + if (i === 0 || i === a.length - 1) { + m.lines.push(l); + return m; + } + m.count += ((l.match(/{{/g) || []).length - (l.match(/}}/g) || []).length); + m.temp.push(l); + if (m.count === 0) { + m.lines.push(m.temp.join(' ')); + m.temp = []; + } + return m; + }, { count: 0, lines: [], temp: [] }) + .lines || []; + let escapeall = ''; + let escaperx = /^\((.+?)\)/g; + let escapeallrx = /^!{{(?:\((.+?)\))?/; + if (escapeallrx.test(lines[0])) { + escapeallrx.lastIndex = 0; + escapeall = escapeallrx.exec(lines[0])[1] || ''; + } + escapeallrx.lastIndex = 0; + lines[0] = lines[0].replace(escapeallrx, ''); // in case there is a command on the first line + lines[lines.length - 1] = lines[lines.length - 1].replace(/}}(?!}})/, ''); // in case there is a command on the last line + lines.filter(l => l.length).forEach(l => { + // handle escape characters + let escapelocal = ''; + escaperx.lastIndex = 0; + if (escaperx.test(l)) { + escaperx.lastIndex = 0; + let eres = escaperx.exec(l); + escapelocal = eres[1]; + l = l.slice(eres[0].length); + } + if (escapeall.length) l = l.replace(new RegExp(escapeRegExp(escapeall), 'g'), ''); + if (escapelocal.length) l = l.replace(new RegExp(escapeRegExp(escapelocal), 'g'), ''); + + if (!/^!/.test(l)) { // this isn't a script message + l = `!${l}{&simple}`; + } + storeOutbound(l); + // dispatchOutbound(l); + + }); + if (batchMsgLibrary[msg.messageID] && batchMsgLibrary[msg.messageID].commands && batchMsgLibrary[msg.messageID].commands.length) { + sendChat('BatchOp', batchMsgLibrary[msg.messageID].commands.shift()); + } + + msg.content = remainingText; + + return; + }; + + // ================================================== + // DEPENDENCIES + // ================================================== + + 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; + }; + + + on('chat:message', handleInput); + + on('ready', () => { + versionInfo(); + logsig(); + let reqs = [ + { + name: 'libInline', + version: `1.0.4`, + mod: typeof libInline !== 'undefined' ? libInline : undefined, + checks: [ + ['getRollData', 'function'], + ['getDice', 'function'], + ['getValue', 'function'], + ['getTables', 'function'], + ['getParsed', 'function'], + ['getRollTip', 'function'] + ] + } + ]; + if (!checkDependencies(reqs)) return; + on('chat:message', handleBatchInput); + + }); + + return { + RegisterMetaOp: registerMetaOp + }; + +})(); +{ try { throw new Error(''); } catch (e) { API_Meta.ZeroFrame.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.ZeroFrame.offset); } } +/* */ \ No newline at end of file diff --git a/ZeroFrame/ZeroFrame.js b/ZeroFrame/ZeroFrame.js index 6836df2fb7..defc90863d 100644 --- a/ZeroFrame/ZeroFrame.js +++ b/ZeroFrame/ZeroFrame.js @@ -3,8 +3,8 @@ Name : ZeroFrame GitHub : https://github.com/TimRohr22/Cauldron/tree/master/ZeroFrame Roll20 Contact : timmaugh -Version : 1.1.10 -Last Update : 12 JUN 2024 +Version : 1.2.2 +Last Update : 6 SEP 2024 ========================================================= */ var API_Meta = API_Meta || {}; @@ -16,9 +16,9 @@ const ZeroFrame = (() => { //eslint-disable-line no-unused-vars // VERSION // ================================================== const apiproject = 'ZeroFrame'; - API_Meta[apiproject].version = '1.1.10'; + API_Meta[apiproject].version = '1.2.2'; const schemaVersion = 0.2; - const vd = new Date(1718196312684); + const vd = new Date(1725629995074); let stateReady = false; const checkInstall = () => { if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { diff --git a/ZeroFrame/script.json b/ZeroFrame/script.json index 71edd26e57..2314346449 100644 --- a/ZeroFrame/script.json +++ b/ZeroFrame/script.json @@ -1,7 +1,7 @@ { "name": "ZeroFrame", "script": "ZeroFrame.js", - "version": "1.2.1", + "version": "1.2.2", "description": "ZeroFrame is a metascript to control metascripts. It cuts in front of other scripts to make sure it handles the API chat message first, then hands it off to registered meta-scripts in a loop, in whatever order is designated by the user. The net effect is that the functional interface of Roll20 is extended, allowing other scripts (and even normal chat messages) to make use of things like conditional logic branching, a wider array of properties that can be retrieved from characters and tokens, inline variables, nested inline rolls, inline math, plugin scriptlets, and selected tokens even when an API script alls an API script.\r\rFor more information, see the wiki entry: \r\r[ZeroFrame Wiki](https://wiki.roll20.net/Script:ZeroFrame) \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", "roll20userid": "5962076", @@ -31,6 +31,7 @@ "1.1.7", "1.1.8", "1.1.9", - "1.2.0" + "1.2.0", + "1.2.1" ] }