From c0b8792ea3fbb856d05e243114b06034f22e0d95 Mon Sep 17 00:00:00 2001 From: timmaugh Date: Mon, 27 Oct 2025 08:51:19 -0400 Subject: [PATCH] ZeroFrame v1.2.3 BatchOps bug fix: restoring who and playerid --- ZeroFrame/1.2.3/ZeroFrame.js | 1350 ++++++++++++++++++++++++++++++++++ ZeroFrame/ZeroFrame.js | 12 +- ZeroFrame/script.json | 5 +- 3 files changed, 1360 insertions(+), 7 deletions(-) create mode 100644 ZeroFrame/1.2.3/ZeroFrame.js diff --git a/ZeroFrame/1.2.3/ZeroFrame.js b/ZeroFrame/1.2.3/ZeroFrame.js new file mode 100644 index 0000000000..f279faeeae --- /dev/null +++ b/ZeroFrame/1.2.3/ZeroFrame.js @@ -0,0 +1,1350 @@ +/* +========================================================= +Name : ZeroFrame +GitHub : https://github.com/TimRohr22/Cauldron/tree/master/ZeroFrame +Roll20 Contact : timmaugh +Version : 1.2.3 +Last Update : 27 OCT 2025 +========================================================= +*/ +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.3'; + const schemaVersion = 0.2; + const vd = new Date(1761568696621); + 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 => ['who', 'playerid'].includes(k) + ? msg[k] = restoreMsg.message[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 defc90863d..f279faeeae 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.2.2 -Last Update : 6 SEP 2024 +Version : 1.2.3 +Last Update : 27 OCT 2025 ========================================================= */ 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.2.2'; + API_Meta[apiproject].version = '1.2.3'; const schemaVersion = 0.2; - const vd = new Date(1725629995074); + const vd = new Date(1761568696621); let stateReady = false; const checkInstall = () => { if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { @@ -1099,7 +1099,9 @@ const ZeroFrame = (() => { //eslint-disable-line no-unused-vars 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]); + .forEach(k => ['who', 'playerid'].includes(k) + ? msg[k] = restoreMsg.message[k] + : msg[k] = msg[k] || restoreMsg.message[k]); } preservedMsgObj[apitrigger] = { message: _.clone(msg), state: initState() }; preserved = preservedMsgObj[apitrigger].message; diff --git a/ZeroFrame/script.json b/ZeroFrame/script.json index 2314346449..6f0c82e7be 100644 --- a/ZeroFrame/script.json +++ b/ZeroFrame/script.json @@ -1,7 +1,7 @@ { "name": "ZeroFrame", "script": "ZeroFrame.js", - "version": "1.2.2", + "version": "1.2.3", "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", @@ -32,6 +32,7 @@ "1.1.8", "1.1.9", "1.2.0", - "1.2.1" + "1.2.1", + "1.2.2" ] }