// ChatSetAttr version 1.9 // Last Updated: 2018-08-24 // A script to create, modify, or delete character attributes from the chat area or macros. // If you don't like my choices for --replace, you can edit the replacers variable at your own peril to change them. /* global log, state, globalconfig, getObj, sendChat, _, getAttrByName, findObjs, createObj, playerIsGM, on */ const ChatSetAttr = (function () { "use strict"; const version = "1.9", observers = { "add": [], "change": [], "destroy": [] }, schemaVersion = 3, replacers = [ [//g, "]"], [/\\rbrak/g, "]"], [/;/g, "?"], [/\\ques/g, "?"], [/`/g, "@"], [/\\at/g, "@"], [/~/g, "-"], [/\\n/g, "\n"], ], // Basic Setup checkInstall = function () { log(`-=> ChatSetAttr v${version} <=-`); if (!state.ChatSetAttr || state.ChatSetAttr.version !== schemaVersion) { log(` > Updating ChatSetAttr Schema to v${schemaVersion} <`); state.ChatSetAttr = { version: schemaVersion, globalconfigCache: { lastsaved: 0 }, playersCanModify: false, playersCanEvaluate: false, useWorkers: true }; } checkGlobalConfig(); }, checkGlobalConfig = function () { const s = state.ChatSetAttr, g = globalconfig && globalconfig.chatsetattr; if (g && g.lastsaved && g.lastsaved > s.globalconfigCache.lastsaved) { log(" > Updating ChatSetAttr from Global Config < [" + (new Date(g.lastsaved * 1000)) + "]"); s.playersCanModify = "playersCanModify" === g["Players can modify all characters"]; s.playersCanEvaluate = "playersCanEvaluate" === g["Players can use --evaluate"]; s.useWorkers = "useWorkers" === g["Trigger sheet workers when setting attributes"]; s.globalconfigCache = globalconfig.chatsetattr; } }, // Utility functions isDef = function (value) { return value !== undefined; }, getWhisperPrefix = function (playerid) { const player = getObj("player", playerid); if (player && player.get("_displayname")) { return "/w \"" + player.get("_displayname") + "\" "; } else { return "/w GM "; } }, sendChatMessage = function (msg, from) { if (from === undefined) from = "ChatSetAttr"; sendChat(from, msg, null, { noarchive: true }); }, setAttribute = function (attr, value) { if (state.ChatSetAttr.useWorkers) attr.setWithWorker(value); else attr.set(value); }, handleErrors = function (whisper, errors) { if (errors.length) { const output = whisper + "
" + "

Errors

" + `

${errors.join("
")}

` + "
"; sendChatMessage(output); errors.splice(0, errors.length); } }, showConfig = function (whisper) { const optionsText = [{ name: "playersCanModify", command: "players-can-modify", desc: "Determines if players can use --name and --charid to " + "change attributes of characters they do not control." }, { name: "playersCanEvaluate", command: "players-can-evaluate", desc: "Determines if players can use the --evaluate option. " + "Be careful in giving players access to this option, because " + "it potentially gives players access to your full API sandbox." }, { name: "useWorkers", command: "use-workers", desc: "Determines if setting attributes should trigger sheet worker operations." }].map(getConfigOptionText).join(""), output = whisper + "
ChatSetAttr Configuration
" + "

!setattr-config can be invoked in the following format:

!setattr-config --option
" + "

Specifying an option toggles the current setting. There are currently two" + " configuration options:

" + optionsText + "
"; sendChatMessage(output); }, getConfigOptionText = function (o) { const button = state.ChatSetAttr[o.name] ? "ON" : "OFF"; return "
${o.name} is currently ${button}` + `Toggle
`; }, getCharNameById = function (id) { const character = getObj("character", id); return (character) ? character.get("name") : ""; }, escapeRegExp = function (str) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); }, htmlReplace = function (str) { const entities = { "<": "lt", ">": "gt", "'": "#39", "*": "#42", "@": "#64", "{": "#123", "|": "#124", "}": "#125", "[": "#91", "]": "#93", "_": "#95", "\"": "quot" }; return String(str).split("").map(c => (entities[c]) ? ("&" + entities[c] + ";") : c).join(""); }, processInlinerolls = function (msg) { if (msg.inlinerolls && msg.inlinerolls.length) { return msg.inlinerolls.map(v => { const ti = v.results.rolls.filter(v2 => v2.table) .map(v2 => v2.results.map(v3 => v3.tableItem.name).join(", ")) .join(", "); return (ti.length && ti) || v.results.total || 0; }) .reduce((m, v, k) => m.replace(`$[[${k}]]`, v), msg.content); } else { return msg.content; } }, notifyAboutDelay = function (whisper) { const chatFunction = () => sendChatMessage(whisper + "Your command is taking a " + "long time to execute. Please be patient, the process will finish eventually."); return setTimeout(chatFunction, 8000); }, getCIKey = function (obj, name) { const nameLower = name.toLowerCase(); let result = false; Object.entries(obj).forEach(([k, ]) => { if (k.toLowerCase() === nameLower) { result = k; } }); return result; }, generateUUID = function () { var a = 0, b = []; return function () { var c = (new Date()).getTime() + 0, d = c === a; a = c; for (var e = new Array(8), f = 7; 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; }; }(), generateRowID = function () { return generateUUID().replace(/_/g, "Z"); }, // Setting attributes happens in a delayed recursive way to prevent the sandbox // from overheating. delayedGetAndSetAttributes = function (whisper, list, setting, errors, rData, opts) { const timeNotification = notifyAboutDelay(whisper), cList = [].concat(list), feedback = [], dWork = function (charid) { const attrs = getCharAttributes(charid, setting, errors, rData, opts); setCharAttributes(charid, setting, errors, feedback, attrs, opts); if (cList.length) { setTimeout(dWork, 50, cList.shift()); } else { clearTimeout(timeNotification); if (!opts.mute) handleErrors(whisper, errors); if (!opts.silent) sendFeedback(whisper, feedback, opts); } }; dWork(cList.shift()); }, setCharAttributes = function (charid, setting, errors, feedback, attrs, opts) { const charFeedback = {}; Object.entries(attrs).forEach(([attrName, attr]) => { let newValue; charFeedback[attrName] = {}; const fillInAttrs = setting[attrName].fillin, settingValue = _.pick(setting[attrName], ["current", "max"]); if (opts.reset) { newValue = { current: attr.get("max") }; } else { newValue = (fillInAttrs) ? _.mapObject(settingValue, v => fillInAttrValues(charid, v)) : Object.assign({}, settingValue); } if (opts.evaluate) { try { newValue = _.mapObject(newValue, function (v) { const parsed = eval(v); if (_.isString(parsed) || Number.isFinite(parsed) || _.isBoolean(parsed)) { return parsed.toString(); } else return v; }); } catch (err) { errors.push("Something went wrong with --evaluate" + ` for the character ${getCharNameById(charid)}.` + ` You were warned. The error message was: ${err}.` + ` Attribute ${attrName} left unchanged.`); return; } } if (opts.mod || opts.modb) { Object.entries(newValue).forEach(([k, v]) => { let moddedValue = parseFloat(v) + parseFloat(attr.get(k) || "0"); if (!_.isNaN(moddedValue)) { if (opts.modb && k === "current") { const parsedMax = parseFloat(attr.get("max")); moddedValue = Math.min(Math.max(moddedValue, 0), _.isNaN(parsedMax) ? Infinity : parsedMax); } newValue[k] = moddedValue; } else { delete newValue[k]; const type = (k === "max") ? "maximum " : ""; errors.push(`Attribute ${type}${attrName} is not number-valued for ` + `character ${getCharNameById(charid)}. Attribute ${type}left unchanged.`); } }); } newValue = _.mapObject(newValue, v => String(v)); charFeedback[attrName] = newValue; const oldAttr = JSON.parse(JSON.stringify(attr)); setAttribute(attr, newValue); notifyObservers("change", attr, oldAttr); }); // Feedback if (!opts.silent) { if ("fb-content" in opts) { const finalFeedback = Object.entries(setting).reduce((m, [attrName, value], k) => { if (!charFeedback[attrName]) return m; else return m.replace(`_NAME${k}_`, attrName) .replace(`_TCUR${k}_`, () => htmlReplace(value.current || "")) .replace(`_TMAX${k}_`, () => htmlReplace(value.max || "")) .replace(`_CUR${k}_`, () => htmlReplace(charFeedback[attrName].current || attrs[attrName].get("current") || "")) .replace(`_MAX${k}_`, () => htmlReplace(charFeedback[attrName].max || attrs[attrName].get("max") || "")); }, String(opts["fb-content"]).replace("_CHARNAME_", getCharNameById(charid))) .replace(/_(?:TCUR|TMAX|CUR|MAX|NAME)\d*_/g, ""); feedback.push(finalFeedback); } else { const finalFeedback = Object.entries(charFeedback).map(([k, o]) => { if ("max" in o && "current" in o) return `${k} to ${htmlReplace(o.current) || "(empty)"} / ${htmlReplace(o.max) || "(empty)"}`; else if ("current" in o) return `${k} to ${htmlReplace(o.current) || "(empty)"}`; else if ("max" in o) return `${k} to ${htmlReplace(o.max) || "(empty)"} (max)`; else return null; }).filter(x => !!x).join(", ").replace(/\n/g, "
"); if (finalFeedback.length) { feedback.push(`Setting ${finalFeedback} for character ${getCharNameById(charid)}.`); } else { feedback.push(`Nothing to do for character ${getCharNameById(charid)}.`); } } } return; }, fillInAttrValues = function (charid, expression) { let match = expression.match(/%(\S.*?)(?:_(max))?%/), replacer; while (match) { replacer = getAttrByName(charid, match[1], match[2] || "current") || ""; expression = expression.replace(/%(\S.*?)(?:_(max))?%/, replacer); match = expression.match(/%(\S.*?)(?:_(max))?%/); } return expression; }, // Getting attributes for a specific character getCharAttributes = function (charid, setting, errors, rData, opts) { const standardAttrNames = Object.keys(setting).filter(x => !setting[x].repeating), rSetting = _.omit(setting, standardAttrNames); return Object.assign({}, getCharStandardAttributes(charid, standardAttrNames, errors, opts), getCharRepeatingAttributes(charid, rSetting, errors, rData, opts) ); }, getCharStandardAttributes = function (charid, attrNames, errors, opts) { const attrs = {}, attrNamesUpper = attrNames.map(x => x.toUpperCase()); if (attrNames.length === 0) return {}; findObjs({ _type: "attribute", _characterid: charid }).forEach(attr => { const nameIndex = attrNamesUpper.indexOf(attr.get("name").toUpperCase()); if (nameIndex !== -1) attrs[attrNames[nameIndex]] = attr; }); _.difference(attrNames, Object.keys(attrs)).forEach(attrName => { if (!opts.nocreate && !opts.deletemode) { attrs[attrName] = createObj("attribute", { characterid: charid, name: attrName }); notifyObservers("add", attrs[attrName]); } else if (!opts.deletemode) { errors.push(`Missing attribute ${attrName} not created for` + ` character ${getCharNameById(charid)}.`); } }); return attrs; }, getCharRepeatingAttributes = function (charid, setting, errors, rData, opts) { const allRepAttrs = {}, attrs = {}, repRowIds = {}, repOrders = {}; if (rData.sections.size === 0) return {}; rData.sections.forEach(prefix => allRepAttrs[prefix] = {}); // Get attributes findObjs({ _type: "attribute", _characterid: charid }).forEach(o => { const attrName = o.get("name"); rData.sections.forEach((prefix, k) => { if (attrName.search(rData.regExp[k]) === 0) { allRepAttrs[prefix][attrName] = o; } else if (attrName === "_reporder_" + prefix) { repOrders[prefix] = o.get("current").split(","); } }); }); // Get list of repeating row ids by prefix from allRepAttrs rData.sections.forEach((prefix, k) => { repRowIds[prefix] = [...new Set(Object.keys(allRepAttrs[prefix]) .map(n => n.match(rData.regExp[k])) .filter(x => !!x) .map(a => a[1]))]; if (repOrders[prefix]) { repRowIds[prefix] = _.chain(repOrders[prefix]) .intersection(repRowIds[prefix]) .union(repRowIds[prefix]) .value(); } }); const repRowIdsLo = _.mapObject(repRowIds, l => l.map(n => n.toLowerCase())); rData.toCreate.forEach(prefix => repRowIds[prefix].push(generateRowID())); Object.entries(setting).forEach(([attrName, value]) => { const p = value.repeating; let finalId; if (isDef(p.rowNum) && isDef(repRowIds[p.splitName[0]][p.rowNum])) { finalId = repRowIds[p.splitName[0]][p.rowNum]; } else if (p.rowIdLo === "-create" && !opts.deletemode) { finalId = repRowIds[p.splitName[0]][repRowIds[p.splitName[0]].length - 1]; } else if (isDef(p.rowIdLo) && repRowIdsLo[p.splitName[0]].includes(p.rowIdLo)) { finalId = repRowIds[p.splitName[0]][repRowIdsLo[p.splitName[0]].indexOf(p.rowIdLo)]; } else if (isDef(p.rowNum)) { errors.push(`Repeating row number ${p.rowNum} invalid for` + ` character ${getCharNameById(charid)}` + ` and repeating section ${p.splitName[0]}.`); } else { errors.push(`Repeating row id ${p.rowIdLo} invalid for` + ` character ${getCharNameById(charid)}` + ` and repeating section ${p.splitName[0]}.`); } if (finalId && p.rowMatch) { const repRowUpper = (p.splitName[0] + "_" + finalId).toUpperCase(); Object.entries(allRepAttrs[p.splitName[0]]).forEach(([name, attr]) => { if (name.toUpperCase().indexOf(repRowUpper) === 0) { attrs[name] = attr; } }); } else if (finalId) { const finalName = p.splitName[0] + "_" + finalId + "_" + p.splitName[1], attrNameCased = getCIKey(allRepAttrs[p.splitName[0]], finalName); if (attrNameCased) { attrs[attrName] = allRepAttrs[p.splitName[0]][attrNameCased]; } else if (!opts.nocreate && !opts.deletemode) { attrs[attrName] = createObj("attribute", { characterid: charid, name: finalName }); notifyObservers("add", attrs[attrName]); } else if (!opts.deletemode) { errors.push(`Missing attribute ${finalName} not created` + ` for character ${getCharNameById(charid)}.`); } } }); return attrs; }, // Deleting attributes delayedDeleteAttributes = function (whisper, list, setting, errors, rData, opts) { const timeNotification = notifyAboutDelay(whisper), cList = [].concat(list), feedback = {}, dWork = function (charid) { const attrs = getCharAttributes(charid, setting, errors, rData, opts); feedback[charid] = []; deleteCharAttributes(charid, attrs, feedback); if (cList.length) { setTimeout(dWork, 50, cList.shift()); } else { clearTimeout(timeNotification); if (!opts.silent) sendDeleteFeedback(whisper, feedback, opts); } }; dWork(cList.shift()); }, deleteCharAttributes = function (charid, attrs, feedback) { Object.keys(attrs).forEach(name => { attrs[name].remove(); notifyObservers("destroy", attrs[name]); feedback[charid].push(name); }); }, // These functions parse the chat input. parseOpts = function (content, hasValue) { // Input: content - string of the form command --opts1 --opts2 value --opts3. // values come separated by whitespace. // hasValue - array of all options which come with a value // Output: object containing key:true if key is not in hasValue. and containing // key:value otherwise return content.replace(//g, "") // delete added HTML line breaks .replace(/\s+$/g, "") // delete trailing whitespace .replace(/\s*{{((?:.|\n)*)\s+}}$/, " $1") // replace content wrapped in curly brackets .replace(/\\([{}])/g, "$1") // add escaped brackets .split(/\s+--/) .slice(1) .reduce((m, arg) => { const kv = arg.split(/\s(.+)/); if (hasValue.includes(kv[0])) { m[kv[0]] = kv[1] || ""; } else { m[arg] = true; } return m; }, {}); }, parseAttributes = function (args, opts, errors) { // Input: args - array containing comma-separated list of strings, every one of which contains // an expression of the form key|value or key|value|maxvalue // replace - true if characters from the replacers array should be replaced // Output: Object containing key|value for all expressions. const globalRepeatingData = { regExp: new Set(), toCreate: new Set(), sections: new Set(), }, setting = args.map(str => { return str.split(/(\\?(?:#|\|))/g) .reduce((m, s) => { if ((s === "#" || s === "|")) m[m.length] = ""; else if ((s === "\\#" || s === "\\|")) m[m.length - 1] += s.slice(-1); else m[m.length - 1] += s; return m; }, [""]); }) .filter(v => !!v) // Replace for --replace .map(arr => { return arr.map((str, k) => { if (opts.replace && k > 0) return replacers.reduce((m, rep) => m.replace(rep[0], rep[1]), str); else return str; }); }) // parse out current/max value .map(arr => { const value = {}; if (arr.length < 3 || arr[1] !== "") { value.current = (arr[1] || "").replace(/^'((?:.|\n)*)'$/, "$1"); } if (arr.length > 2) { value.max = arr[2].replace(/^'((?:.|\n)*)'$/, "$1"); } return [arr[0], value]; }) // Find out if we need to run %_% replacement .map(([name, value]) => { if ((value.current && value.current.search(/%(\S.*?)(?:_(max))?%/) !== -1) || (value.max && value.max.search(/%(\S.*?)(?:_(max))?%/) !== -1)) value.fillin = true; else value.fillin = false; return [name, value]; }) // Do repeating section stuff .map(([name, value]) => { if (name.search(/^repeating_/) === 0) { value.repeating = getRepeatingData(name, globalRepeatingData, opts, errors); } else value.repeating = false; return [name, value]; }) .filter(([, value]) => value.repeating !== null) .reduce((p, c) => { p[c[0]] = Object.assign(p[c[0]] || {}, c[1]); return p; }, {}); globalRepeatingData.sections.forEach(s => { globalRepeatingData.regExp.add(new RegExp(`^${escapeRegExp(s)}_(-[-A-Za-z0-9]+?|\\d+)_`, "i")); }); globalRepeatingData.regExp = [...globalRepeatingData.regExp]; globalRepeatingData.toCreate = [...globalRepeatingData.toCreate]; globalRepeatingData.sections = [...globalRepeatingData.sections]; return [setting, globalRepeatingData]; }, getRepeatingData = function (name, globalData, opts, errors) { const match = name.match(/_(\$\d+|-[-A-Za-z0-9]+|\d+)(_)?/); let output = {}; if (match && match[1][0] === "$" && match[2] === "_") { output.rowNum = parseInt(match[1].slice(1)); } else if (match && match[2] === "_") { output.rowId = match[1]; output.rowIdLo = match[1].toLowerCase(); } else if (match && match[1][0] === "$" && opts.deletemode) { output.rowNum = parseInt(match[1].slice(1)); output.rowMatch = true; } else if (match && opts.deletemode) { output.rowId = match[1]; output.rowIdLo = match[1].toLowerCase(); output.rowMatch = true; } else { errors.push(`Could not understand repeating attribute name ${name}.`); output = null; } if (output) { output.splitName = name.split(match[0]); globalData.sections.add(output.splitName[0]); if (output.rowIdLo === "-create" && !opts.deletemode) { globalData.toCreate.add(output.splitName[0]); } } return output; }, // These functions are used to get a list of character ids from the input, // and check for permissions. checkPermissions = function (list, errors, playerid, isGM) { return list.filter(id => { const character = getObj("character", id); if (character) { const control = character.get("controlledby").split(/,/); if (!(isGM || control.includes("all") || control.includes(playerid) || state.ChatSetAttr.playersCanModify)) { errors.push(`Permission error for character ${character.get("name")}.`); return false; } else return true; } else { errors.push(`Invalid character id ${id}.`); return false; } }); }, getIDsFromTokens = function (selected) { return (selected || []).map(obj => getObj("graphic", obj._id)) .filter(x => !!x) .map(token => token.get("represents")) .filter(id => getObj("character", id || "")); }, getIDsFromNames = function (charNames, errors) { return charNames.split(/\s*,\s*/) .map(name => { const character = findObjs({ _type: "character", name: name }, { caseInsensitive: true })[0]; if (character) { return character.id; } else { errors.push(`No character named ${name} found.`); return null; } }) .filter(x => !!x); }, sendFeedback = function (whisper, feedback, opts) { const output = (opts["fb-public"] ? "" : whisper) + "
" + "

" + (("fb-header" in opts) ? opts["fb-header"] : "Setting attributes") + "

" + "

" + (feedback.join("
") || "Nothing to do.") + "

"; sendChatMessage(output, opts["fb-from"]); }, sendDeleteFeedback = function (whisper, feedback, opts) { let output = (opts["fb-public"] ? "" : whisper) + "
" + "

" + (("fb-header" in opts) ? opts["fb-header"] : "Deleting attributes") + "

"; output += Object.entries(feedback) .filter(([, arr]) => arr.length) .map(([charid, arr]) => `Deleting attribute(s) ${arr.join(", ")} for character ${getCharNameById(charid)}.`) .join("
") || "Nothing to do."; output += "

"; sendChatMessage(output, opts["fb-from"]); }, handleCommand = (content, playerid, selected, pre) => { // Parsing input let charIDList = [], errors = []; const hasValue = ["charid", "name", "fb-header", "fb-content", "fb-from"], optsArray = ["all", "allgm", "charid", "name", "allplayers", "sel", "deletemode", "replace", "nocreate", "mod", "modb", "evaluate", "silent", "reset", "mute", "fb-header", "fb-content", "fb-from", "fb-public" ], opts = parseOpts(content, hasValue), isGM = playerid === "API" || playerIsGM(playerid), whisper = getWhisperPrefix(playerid); opts.mod = opts.mod || (pre === "mod"); opts.modb = opts.modb || (pre === "modb"); opts.reset = opts.reset || (pre === "reset"); opts.silent = opts.silent || opts.mute; opts.deletemode = (pre === "del"); // Sanitise feedback if ("fb-from" in opts) opts["fb-from"] = String(opts["fb-from"]); // Parse desired attribute values const [setting, rData] = parseAttributes(Object.keys(_.omit(opts, optsArray)), opts, errors); // Fill in header info if ("fb-header" in opts) { opts["fb-header"] = Object.entries(setting).reduce((m, [n, v], k) => { return m.replace(`_NAME${k}_`, n) .replace(`_TCUR${k}_`, htmlReplace(v.current || "")) .replace(`_TMAX${k}_`, htmlReplace(v.max || "")); }, String(opts["fb-header"])).replace(/_(?:TCUR|TMAX|NAME)\d*_/g, ""); } if (opts.evaluate && !isGM && !state.ChatSetAttr.playersCanEvaluate) { if (!opts.mute) handleErrors(whisper, ["The --evaluate option is only available to the GM."]); return; } // Get list of character IDs if (opts.all && isGM) { charIDList = findObjs({ _type: "character" }).map(c => c.id); } else if (opts.allgm && isGM) { charIDList = findObjs({ _type: "character" }).filter(c => c.get("controlledby") === "") .map(c => c.id); } else if (opts.allplayers && isGM) { charIDList = findObjs({ _type: "character" }).filter(c => c.get("controlledby") !== "") .map(c => c.id); } else { if (opts.charid) charIDList.push(...opts.charid.split(/\s*,\s*/)); if (opts.name) charIDList.push(...getIDsFromNames(opts.name, errors)); if (opts.sel) charIDList.push(...getIDsFromTokens(selected)); charIDList = checkPermissions([...new Set(charIDList)], errors, playerid, isGM); } if (charIDList.length === 0) { errors.push("No target characters. You need to supply one of --all, --allgm, --sel," + " --allplayers, --charid, or --name."); } if (Object.keys(setting).length === 0) { errors.push("No attributes supplied."); } // Get attributes if (!opts.mute) handleErrors(whisper, errors); // Set or delete attributes if (charIDList.length > 0 && Object.keys(setting).length > 0) { if (opts.deletemode) { delayedDeleteAttributes(whisper, charIDList, setting, errors, rData, opts); } else { delayedGetAndSetAttributes(whisper, charIDList, setting, errors, rData, opts); } } }, handleInlineCommand = (msg) => { const command = msg.content.match(/!(set|mod|modb)attr .*?!!!/); if (command) { const mode = command[1], newMsgContent = command[0].slice(0, -3).replace(/{{[^}[\]]+\$\[\[(\d+)\]\].*?}}/g, (_, number) => { return `$[[${number}]]`; }); const newMsg = { content: newMsgContent, inlinerolls: msg.inlinerolls, }; handleCommand( processInlinerolls(newMsg), msg.playerid, msg.selected, mode ); } }, // Main function, called after chat message input handleInput = function (msg) { if (msg.type !== "api") handleInlineCommand(msg); else { const mode = msg.content.match(/^!(reset|set|del|mod|modb)attr\b(?:-|\s|$)(config)?/); if (mode && mode[2]) { if (playerIsGM(msg.playerid)) { const whisper = getWhisperPrefix(msg.playerid), opts = parseOpts(msg.content, []); if (opts["players-can-modify"]) { state.ChatSetAttr.playersCanModify = !state.ChatSetAttr.playersCanModify; } if (opts["players-can-evaluate"]) { state.ChatSetAttr.playersCanEvaluate = !state.ChatSetAttr.playersCanEvaluate; } if (opts["use-workers"]) { state.ChatSetAttr.useWorkers = !state.ChatSetAttr.useWorkers; } showConfig(whisper); } } else if (mode) { handleCommand( processInlinerolls(msg), msg.playerid, msg.selected, mode[1] ); } } return; }, notifyObservers = function(event, obj, prev) { observers[event].forEach(observer => observer(obj, prev)); }, registerObserver = function (event, observer) { if(observer && _.isFunction(observer) && observers.hasOwnProperty(event)) { observers[event].push(observer); } else { log("ChatSetAttr event registration unsuccessful. Please check the documentation."); } }, registerEventHandlers = function () { on("chat:message", handleInput); }; return { checkInstall, registerObserver, registerEventHandlers }; }()); on("ready", function () { "use strict"; ChatSetAttr.checkInstall(); ChatSetAttr.registerEventHandlers(); });