// 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 "" +
"- " +
"
${button}
` +
`${o.command}${htmlReplace("-")}` +
`${o.desc}
${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();
});