// ChatSetAttr version 1.1.1
// Last Updated: 2016-11-4
// 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.
var chatSetAttr = chatSetAttr || (function() {
'use strict';
const version = '1.1.1',
replacers = [ ['<', '[', /',']' , />/g, /\]/g],
['#','|', /#/g, /\|/g],
['~','-', /\~/g, /\-/g],
[';','?', /\;/g, /\?/g],
['`','@', /`/g, /@/g]],
checkInstall = function() {
log(`-=> ChatSetAttr v${version} <=-`);
},
handleErrors = function(who, errors) {
if (errors.length) {
let output = `/w "${who}"
`
+ `
Errors
${errors.join('
')}
`;
sendChat('ChatSetAttr', output);
errors.splice(0, errors.length);
}
},
getPlayerName = function(who) {
let match = who.match(/(.*) \(GM\)/);
return (match) ? (match[1] || 'GM') : who;
},
escapeRegExp = function (str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
htmlReplace = function (str) {
let entities = {
'<' : 'lt',
'>' : 'gt',
"'" : '#39',
'@' : '#64',
'{' : '#123',
'|' : '#124',
'}' : '#125',
'[' : '#91',
']' : '#93',
'"' : 'quot',
'-' : 'mdash',
' ' : 'nbsp'
};
return _.chain(str.split(''))
.map(c => (_.has(entities,c)) ? ('&'+entities[c]+';') : c)
.value()
.join('');
},
processInlinerolls = function (msg) {
// Input: msg - chat message
// Output: msg.content, with all inline rolls evaluated
if (_.has(msg, 'inlinerolls')) {
return _.chain(msg.inlinerolls)
.reduce(function(previous, current, index) {
previous['$[[' + index + ']]'] = current.results.total || 0;
return previous;
},{})
.reduce(function(previous, current, index) {
return previous.replace(index, current);
}, msg.content)
.value();
} else {
return msg.content;
}
},
getCIKey = function (obj, name) {
let nameLower = name.toLowerCase(), result = false;
_.each(obj, function (v,k) {
if (k.toLowerCase() === nameLower) {
result = k;
}
});
return result;
},
// Getting attributes from parsed options. Repeating attributes need special treatment
// in order to parse row index and not create defective repeating rows.
getRepeatingAttributes = function(list, setting, errors, createMissing, failSilently) {
let allAttrs = {}, allKeys = _.keys(setting), indexMatch, attrNameSplit, id, name,
repeatingTypeStart, allSectionAttrs, repSectionIds, rowNum, rowId, idMatch,
repSectionIdsLower, realRepName;
list.forEach(function(charid) {
allAttrs[charid] = {};
});
_.each(allKeys, function(attrName) {
indexMatch = attrName.match(/_\$(\d+)_/);
allSectionAttrs = {}, repSectionIds = {}, repSectionIdsLower = {};
_.each(list, function(charid) {
allSectionAttrs[charid] = {};
});
if (indexMatch) {
rowNum = parseInt(indexMatch[1]);
attrNameSplit = attrName.split(indexMatch[0]);
} else {
idMatch = attrName.match(/_(-[-A-Za-z0-9]+?|\d+)_/);
if (idMatch) {
rowId = idMatch[1].toLowerCase();
attrNameSplit = attrName.split(idMatch[0]);
} else {
errors.push(`Could not understand repeating attribute name`
+ ` ${attrName}.`);
return;
}
}
repeatingTypeStart = new RegExp('^' + escapeRegExp(attrNameSplit[0])
+ '_(-[-A-Za-z0-9]+?|\\d+)_','i');
filterObjs(function(o) {
if (o.get('_type') === 'attribute') {
id = o.get('_characterid');
name = o.get('name');
if (_.contains(list,id) && name.search(repeatingTypeStart) !== -1) {
allSectionAttrs[id][name] = o;
return true;
}
}
});
_.each(list, function(charid) {
repSectionIds[charid] = _.chain(allSectionAttrs[charid])
.map((o,n) => n.match(repeatingTypeStart))
.compact()
.map(a => a[1])
.uniq()
.value();
if (!indexMatch) {
repSectionIdsLower[charid] = _.map(repSectionIds[charid],
n => n.toLowerCase());
}
});
_.each(list, function(charid) {
if (indexMatch && !_.isUndefined(repSectionIds[charid][rowNum])) {
realRepName = attrNameSplit[0] + '_' + repSectionIds[charid][rowNum]
+ '_' + attrNameSplit[1];
} else if (!indexMatch && _.contains(repSectionIdsLower[charid], rowId)) {
realRepName = attrNameSplit[0] + '_'
+ repSectionIds[charid][_.indexOf(repSectionIdsLower[charid], rowId)]
+ '_' + attrNameSplit[1];
} else if (indexMatch) {
errors.push(`Row number ${rowNum} invalid for character`
+ ` ${getAttrByName(charid,'character_name')}`
+ ` and repeating section ${attrNameSplit[0]}.`);
return;
} else {
errors.push(`Repeating section id ${rowId} invalid for character`
+ ` ${getAttrByName(charid,'character_name')}`
+ ` and repeating section ${attrNameSplit[0]}.`);
return;
}
let nameCI = getCIKey(allSectionAttrs[charid], realRepName);
if (nameCI) {
allAttrs[charid][attrName] = allSectionAttrs[charid][nameCI];
} else if (createMissing) {
allAttrs[charid][attrName] = createObj('attribute',
{characterid: charid , name: realRepName});
} else if (!failSilently) {
errors.push(`Missing attribute ${realRepName} not created for`
+ ` character ${getAttrByName(charid,'character_name')}.`);
}
});
});
return allAttrs;
},
getStandardAttributes = function(list, setting, errors, createMissing, failSilently) {
let allAttrs = {}, allKeys = _.keys(setting), allKeysUpper, id, name;
allKeysUpper = allKeys.map(x => x.toUpperCase());
list.forEach(function(charid) {
allAttrs[charid] = {};
});
filterObjs(function(o) {
if (o.get('_type') === 'attribute') {
id = o.get('_characterid');
name = o.get('name');
if (_.contains(list,id) && _.contains(allKeysUpper,name.toUpperCase())) {
allAttrs[id][allKeys[_.indexOf(allKeysUpper, name.toUpperCase())]] = o;
return true;
}
}
});
list.forEach(function(charid) {
_.each(_.difference(allKeys, _.keys(allAttrs[charid])), function (key) {
if (createMissing) {
allAttrs[charid][key] = createObj('attribute', {characterid: charid , name: key});
}
else if (!failSilently) {
errors.push(`Missing attribute ${key} not created for character`
+ ` ${getAttrByName(charid,'character_name')}.`);
}
});
});
return allAttrs;
},
getAllAttributes = function(list, setting, errors, createMissing, failSilently) {
let settingRepeating = _.pick(setting, (v,k) => (k.search(/^repeating_/) !== -1));
let settingStandard = _.omit(setting, _.keys(settingRepeating));
let standardAttrs = getStandardAttributes(list, settingStandard, errors, createMissing, failSilently);
let repeatingAttrs = getRepeatingAttributes(list, settingRepeating, errors, createMissing, failSilently);
let allAttrs = {};
_.each(list, function(charid) {
allAttrs[charid] = _.defaults(standardAttrs[charid],repeatingAttrs[charid]);
});
return allAttrs;
},
// Setting attributes happens in a delayed recursive way to prevent the sandbox
// from overheating.
delayedSetAttributes = function(who, list, setting, errors, allAttrs, fillInAttrs, opts) {
let cList = _.clone(list), feedback = [],
dWork = function(charid) {
setCharAttributes(charid, setting, errors, feedback, allAttrs[charid],
fillInAttrs, opts);
if (cList.length) {
_.delay(dWork, 50, cList.shift());
} else {
handleErrors(who, errors);
if (!opts.silent) {
sendFeedback(who, feedback);
}
}
}
dWork(cList.shift());
},
setCharAttributes = function(charid, setting, errors, feedback, attrs, fillInAttrs, opts) {
let charFeedback = {};
_.chain(setting)
.pick(_.keys(attrs))
.each(function (attrValue,attrName) {
let attr = attrs[attrName];
let attrNew = (fillInAttrs[attrName]) ?
_.mapObject(attrValue, v => fillInAttrValues(charid, v)) : _.clone(attrValue);
if (opts.evaluate) {
try {
attrNew = _.mapObject(attrNew, function (v) {
let parsed = eval(v);
if (!_.isNaN(parsed) && !_.isUndefined(parsed)) {
return parsed.toString();
}
else return v;
});
}
catch(err) {
errors.push(`Something went wrong with --evaluate.`
+ ` You were warned. The error message was: ${err}.`);
}
}
if (opts.mod || opts.modb) {
_.each(attrNew, function(v,k) {
let moddedValue = parseFloat(v) + parseFloat(attr.get(k) || '0');
if (!_.isNaN(moddedValue)) {
if (opts.modb && k === 'current') {
moddedValue = Math.min(Math.max(moddedValue, 0),
parseFloat(attr.get('max') || Infinity));
}
attrNew[k] = moddedValue.toString();
}
else {
delete attrNew[k];
let type = (k === 'max') ? 'maximum ' : '';
errors.push(`Attribute ${type}${attrName} is not number-valued`
+ ` for character ${getAttrByName(charid,'character_name')}`
+ `. Attribute ${type}left unchanged.`);
}
});
}
charFeedback[attrName] = attrNew;
attr.set(attrNew);
// attr.setWithWorker(attrNew);
});
// Feedback
charFeedback = _.chain(charFeedback)
.mapObject(function (o,k,l) {
if (!_.isUndefined(o.max) && !_.isUndefined(o.current))
return `${htmlReplace(o.current) || '(empty)'} / ${htmlReplace(o.max) || '(empty)'}`;
else if (!_.isUndefined(o.current)) return htmlReplace(o.current) || '(empty)';
else if (!_.isUndefined(o.max)) return `${htmlReplace(o.max) || '(empty)'} (max)`;
else return null;
})
.omit(_.isNull)
.value();
if (!_.isEmpty(charFeedback)) {
feedback.push(`Setting ${_.keys(charFeedback).join(', ')} to`
+ ` ${_.values(charFeedback).join(', ')} for character`
+ ` ${getAttrByName(charid, 'character_name')}.`);
} else {
feedback.push(`Nothing to do for character`
+ ` ${getAttrByName(charid, 'character_name')}.`);
}
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;
},
deleteAttributes = function (who, allAttrs, silent) {
let feedback = {};
_.each(allAttrs, function(charAttrs, charid) {
feedback[charid] = [];
_.each(charAttrs, function(attr, name) {
attr.remove();
feedback[charid].push(name);
});
});
silent ? null : sendDeleteFeedback(who, feedback);
},
// 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
let args, kv, opts = {};
args = _.rest(content.replace(/
\n/g, ' ')
.replace(/\s*$/g, '')
.replace(/({{(.*?)\s*}}$)/g, '$2')
.split(/\s+--/));
for (let k in args) {
kv = args[k].split(/\s(.+)/);
if (_.contains(hasValue, kv[0])) {
opts[kv[0]] = kv[1];
} else {
opts[args[k]] = true;
}
}
return opts;
},
parseAttributes = function(args, replace, fillInAttrs) {
// 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.
let setting = _.chain(args)
.map(str => str.split('').reverse().join('')
.split(/\s*\|(?!\\)\s*/).reverse()
.map(str => str.split('').reverse().join('')))
.reject(a => a.length === 0)
.map(sanitizeAttributeArray)
.reduce(function (p,c) {
p[c[0]] = _.extend(p[c[0]] || {}, c[1])
return p;
},{})
.value();
if (replace) {
setting = _.mapObject(setting, function(obj) {
return _.mapObject(obj, function (str) {
_.each(replacers, function (rep) {
str = str.replace(rep[2],rep[1]);
});
return str;
});
});
}
_.extend(fillInAttrs, _.mapObject(setting, obj =>
(obj.current && obj.current.search(/%(\S.*?)(?:_(max))?%/) !== -1)
|| (obj.max && obj.max.search(/%(\S.*?)(?:_(max))?%/) !== -1)
));
return setting;
},
sanitizeAttributeArray = function (arr) {
if (arr.length === 1)
return [arr[0],{current : ''}];
if (arr.length === 2)
return [arr[0],{current : arr[1].replace(/^'(.*)'$/,'$1')}];
if (arr.length === 3 && arr[1] === '')
return [arr[0], {max : arr[2].replace(/^'(.*)'$/,'$1')}];
if (arr.length === 3 && arr[1] === "''")
return [arr[0], {current : '', max : arr[2].replace(/^'(.*)'$/,'$1')}];
else if (arr.length === 3)
return [arr[0], {current : arr[1].replace(/^'(.*)'$/,'$1'), max : arr[2].replace(/^'(.*)'$/,'$1')}];
if (arr.length > 3) return sanitizeAttributeArray(_.first(arr,3));
},
// These functions are used to get a list of character ids from the input,
// and check for permissions.
checkPermissions = function (list, errors, playerid) {
let control, character, remove = [];
_.each(list, function (id, k) {
character = getObj('character', id);
if (character) {
control = character.get('controlledby').split(/,/);
if(!(playerIsGM(playerid) || _.contains(control,'all') || _.contains(control,playerid))) {
remove.push(k);
errors.push(`Permission error for character ${character.get('name')}.`);
}
} else {
errors.push(`Invalid character id ${id}.`);
remove.push(k);
}
});
_.each(remove.reverse(), i => list.splice(i,1));
return list;
},
getIDsFromTokens = function (selected) {
return _.chain(selected)
.map(obj => getObj('graphic', obj._id))
.compact()
.map(token => token.get('represents'))
.compact()
.filter(id => getObj('character', id))
.uniq()
.value();
},
getIDsFromNames = function(charNames, errors, playerid) {
let charIDList = _.chain(charNames.split(/\s*,\s*/))
.map(n => [n, findObjs({type: 'character', name: n},
{caseInsensitive: true})[0]])
.each(function (arr) {
_.isUndefined(arr[1]) ? errors.push('No character named '
+ arr[0] + ' found.') : null;
})
.map(arr => arr[1])
.compact()
.map(c => c.id)
.uniq()
.value();
return checkPermissions(charIDList, errors, playerid);
},
getIDsFromList = function(charid, errors, playerid) {
return checkPermissions(_.uniq(charid.split(/\s*,\s*/)), errors, playerid);
},
sendFeedback = function(who, feedback) {
let output = `/w "${who}" Setting attributes
';
output += feedback.join('
') || 'Nothing to do.';
output += '
';
sendChat('ChatSetAttr', output);
},
sendDeleteFeedback = function (who, feedback) {
let output = `/w "${who}" Deleting attributes
';
output += _.chain(feedback)
.omit(arr => _.isEmpty(arr))
.map(function (arr,id) {
return `Deleting attribute(s) ${arr.join(', ')} for character`
+ ` ${getAttrByName(id, 'character_name')}.`;
})
.join('
')
.value() || 'Nothing to do.';
output += '
';
sendChat('ChatSetAttr', output);
},
// Main function, called after chat message input
handleInput = function(msg) {
if (msg.type !== 'api') {
return;
}
let mode = msg.content.match(/^!(set|del)attr\b/);
if (mode) {
// Parsing input
let charIDList = [], fillInAttrs = {}, errors = [];
const hasValue = ['charid','name'],
optsArray = ['all','allgm','charid','name','silent','sel',
'replace', 'nocreate','mod','modb','evaluate'],
who = getPlayerName(msg.who),
opts = parseOpts(processInlinerolls(msg),hasValue),
setting = parseAttributes(_.chain(opts).omit(optsArray).keys().value(),
opts.replace, fillInAttrs),
deleteMode = (mode[1] === 'del');
if (opts.evaluate && !playerIsGM(msg.playerid)) {
errors.push('The --evaluate option is only available to the GM.');
delete opts.evaluate;
}
// Get list of character IDs
if (opts.all && playerIsGM(msg.playerid)) {
charIDList = _.map(findObjs({_type: 'character'}), c => c.id);
} else if (opts.allgm && playerIsGM(msg.playerid)) {
charIDList = _.chain(findObjs({_type: 'character'}))
.filter(c => c.get('controlledby') === '')
.map(c => c.id)
.value();
} else if (opts.charid) {
charIDList = getIDsFromList(opts.charid, errors, msg.playerid);
} else if (opts.name) {
charIDList = getIDsFromNames(opts.name, errors, msg.playerid);
} else if (opts.sel) {
charIDList = getIDsFromTokens(msg.selected);
} else {
errors.push('You need to supply one of --all, --allgm, --sel,'
+ ' --charid, or --name.');
}
if (_.isEmpty(charIDList)) {
errors.push('No target characters.');
}
if (_.isEmpty(setting)) {
errors.push('No attributes supplied.');
}
// Get attributes
let allAttrs = getAllAttributes(charIDList, setting, errors, !opts.nocreate && !deleteMode, deleteMode);
handleErrors(who, errors);
// Set or delete attributes
if (!_.isEmpty(charIDList) && !_.isEmpty(setting)) {
if (deleteMode) {
deleteAttributes(who, allAttrs, opts.silent);
} else {
delayedSetAttributes(who, charIDList, setting, errors, allAttrs,
fillInAttrs, _.pick(opts, optsArray));
}
}
}
return;
},
registerEventHandlers = function() {
on('chat:message', handleInput);
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
};
}());
on('ready',function() {
'use strict';
chatSetAttr.CheckInstall();
chatSetAttr.RegisterEventHandlers();
});