Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Sharing Utility Functions

1418153726
Lithl
Pro
Sheet Author
API Scripter
getPlayerCharacterFrom(name) Dependencies: None Given a string name, this function returns an appropriate value for the first parameter of sendChat . function getPlayerCharacterFrom(name) { var character = findObjs({ type: 'character', name: name })[0], player = findObjs({ type: 'player', displayname: name.lastIndexOf(' (GM)') === name.length - 5 ? name.substring(0, name.length - 5) : name })[0]; if (player) { return 'player|' + player.id; } if (character) { return 'character|' + character.id; } return name; } // example use: // sendChat(getPlayerCharacterFrom(msg.who), 'What\'s up, doc?'); getWhisperTarget(options) Dependencies: levenshteinDistance (see below) Given a set of options, this function tries to construct the "/w name " portion of a whisper for a call to sendChat . The options parameter should contain either player: true or character: true and a value for either id or name . Players are preferred over characters if both are true, and ids are preferred over names if both have a valid value. If a name is supplied, the player or character with the name closest to the supplied string will be sent the whisper. options is technically optional, but if you omit it (or don't supply a combination of player/character + id/name), the function will return an empty string. function getWhisperTarget(options) { var nameProperty, targets, type; options = options || {}; if (options.player) { nameProperty = 'displayname'; type = 'player'; } else if (options.character) { nameProperty = 'name'; type = 'character'; } else { return ''; } if (options.id) { targets = [getObj(type, options.id)]; if (targets[0]) { return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' '; } } if (options.name) { // Gets all players or characters (as appropriate) whose name *contains* the supplied name, // then sorts them by how close they are to the supplied name. targets = _.sortBy(filterObjs(function(obj) { if (obj.get('type') !== type) return false; return obj.get(nameProperty).indexOf(options.name) &gt;= 0; }), function(obj) { return Math.abs(levenshteinDistance(obj.get(nameProperty), options.name)); }); if (targets[0]) { return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' '; } } return ''; } // example use: // sendChat('', getWhisperTarget({ player: true, id: msg.playerid }) + 'You shouldn\'t do that'); // sendChat('', getWhisperTarget({ player: true, name: msg.who }) + 'You\'ll go blind'); // sendChat('', getWhisperTarget({ character: true, id: myCharacter.id }) + 'That\'s illegal'); // sendChat('', getwhisperTarget({ character: true, name: myCharacter.get('name') }) + 'And dangerous'); splitArgs(input, separator) Source: <a href="https://github.com/elgs/splitargs" rel="nofollow">https://github.com/elgs/splitargs</a> While msg.content.split(' ') is often sufficient, sometimes you want a little more control over how you split up parameters to your API command. This function lets you split up a string while maintaining quoted sections using either single or double quotes. separator is optional, and if omitted the function will default to splitting around whitespace. If supplied, separator should be a regular expression object, not a string. function splitArgs(input, separator) { var singleQuoteOpen = false, doubleQuoteOpen = false, tokenBuffer = [], ret = [], arr = input.split(''), element, i, matches; separator = separator || /\s/g; for (i = 0; i &lt; arr.length; i++) { element = arr[i]; matches = element.match(separator); if (element === '\'') { if (!doubleQuoteOpen) { singleQuoteOpen = !singleQuoteOpen; continue; } } else if (element === '"') { if (!singleQuoteOpen) { doubleQuoteOpen = !doubleQuoteOpen; continue; } } if (!singleQuoteOpen && !doubleQuoteOpen) { if (matches) { if (tokenBuffer && tokenBuffer.length &gt; 0) { ret.push(tokenBuffer.join('')); tokenBuffer = []; } } else { tokenBuffer.push(element); } } else if (singleQuoteOpen || doubleQuoteOpen) { tokenBuffer.push(element); } } if (tokenBuffer && tokenBuffer.length &gt; 0) { ret.push(tokenBuffer.join('')); } return ret; } // example use: // parameters = splitArgs(msg.content); levenshteinDistance(a, b) Source: <a href="http://en.wikibooks.org/wiki/Algorithm_Implementat" rel="nofollow">http://en.wikibooks.org/wiki/Algorithm_Implementat</a>... Computes the number of transformations required to get from string a to string b . A transformation can be substitution (ABC -&gt; ADC), insertion (ABC -&gt; ABDC), or deletion (ABC -&gt; AC). function levenshteinDistance(a, b) { var i, j, matrix = []; if (a.length === 0) { return b.length; } if (b.length === 0) { return a.length; } // Increment along the first column of each row for (i = 0; i &lt;= b.length; i++) { matrix[i] = [i]; } // Increment each column in the first row for (j = 0; j &lt;= a.length; j++) { matrix[0][j] = j; } // Fill in the rest of the matrix for (i = 1; i &lt;= b.length; i++) { for (j = 1; j &lt;= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution Math.min(matrix[i][j - 1] + 1, // Insertion matrix[i - 1][j] + 1)); // Deletion } } } return matrix[b.length][a.length]; } // example use (see getWhisperTarget(options), above): // Math.abs(levenshteinDistance(obj.get(nameProperty), options.name)) How about the rest of you? Any awesome utility functions that could be used in most any script? Preferably things with limited or no dependencies on other functions, other than Underscore.js, native JavaScript, or built-in Roll20 library functions.
1418157593
The Aaron
Forum Champion
API Scripter
It doesn't support custom separators, but here is another function that splits arguments retaining quoted sections: function splitArgs(input) { return _.map(input.match(/'[^']+'|"[^"]+"|[^\s]+/g),function(a){return a.replace(/'([^']+(?='))'/g,'$1').replace(/"([^"]+(?="))"/g,'$1');}); } Here's a snippet I use frequently to replace all the inline rolls in a command with their values (you want to be sure and clone the msg if you are doing this so you don't change it for everyone.): if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } This takes a status string and turns it into an object: statusmarkersToObject = function(stats) { return _.reduce(stats.split(/,/),function(memo,st){ var parts=st.split(/@/), num=parseInt(parts[1]||'0',10); if(parts[0].length) { memo[parts[0]]=Math.max(num,memo[parts[0]]||0); } return memo; },{}); }; Not so polished as your descriptions... maybe I'll come back and edit later.. =D
1418157656
I've got a Shell module (<a href="https://github.com/manveti/roll20/blob/master/shell.js" rel="nofollow">https://github.com/manveti/roll20/blob/master/shell.js</a>) which has some basic I/O functions (write to chat with some basic styling for readability, tokenize a command like a POSIX shell command -- similarly to your splitArgs function) and handles command-marshalling so each script doesn't have to write its own on("chat:message") callback. There's a built-in "!help" function which will list all registered commands, meaningful error messages when someone tries to register a command with the same name as an already-registered command, and support for permissions (using Aaron's isGM module if present, or falling back on its own heuristics if not).
1418174164
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: It doesn't support custom separators, but here is another function that splits arguments retaining quoted sections: function splitArgs(input) { return _.map(input.match(/'[^']+'|"[^"]+"|[^\s]+/g),function(a){return a.replace(/'([^']+(?='))'/g,'$1').replace(/"([^"]+(?="))"/g,'$1');}); } This is dropping nested double+single quotes splitArgs_Aaron('Talk to "Jerry \'Dead Fish\' Adams"') // yields: // ["Talk", // "to", // "Jerry Dead Fish Adams"] splitArgs_Wiki('Talk to "Jerry \'Dead Fish\' Adams"') // yields: // ["Talk", // "to", // "Jerry 'Dead Fish' Adams"] The same happens to double quotes inside single quotes. The Aaron said: Here's a snippet I use frequently to replace all the inline rolls in a command with their values (you want to be sure and clone the msg if you are doing this so you don't change it for everyone.): if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } Is there any particular reason you're favoring _.has over simple boolean coersion? if (msg.inlinerolls) should work perfectly fine in all cases.
1418189567
The Aaron
Forum Champion
API Scripter
Brian said: The Aaron said: It doesn't support custom separators, but here is another function that splits arguments retaining quoted sections: function splitArgs(input) { return _.map(input.match(/'[^']+'|"[^"]+"|[^\s]+/g),function(a){return a.replace(/'([^']+(?='))'/g,'$1').replace(/"([^"]+(?="))"/g,'$1');}); } This is dropping nested double+single quotes splitArgs_Aaron('Talk to "Jerry \'Dead Fish\' Adams"') // yields: // ["Talk", // "to", // "Jerry Dead Fish Adams"] splitArgs_Wiki('Talk to "Jerry \'Dead Fish\' Adams"') // yields: // ["Talk", // "to", // "Jerry 'Dead Fish' Adams"] The same happens to double quotes inside single quotes. Interesting case, corrected: function splitArgs(input) { return _.map(input.match(/'[^']+'|"[^"]+"|[^\s]+/g),function(a){return a.replace(/^'([^']+(?='))'$/g,'$1').replace(/^"([^"]+(?="))"$/g,'$1');}); } Brian said: The Aaron said: Here's a snippet I use frequently to replace all the inline rolls in a command with their values (you want to be sure and clone the msg if you are doing this so you don't change it for everyone.): if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } Is there any particular reason you're favoring _.has over simple boolean coersion? if (msg.inlinerolls) should work perfectly fine in all cases. It wouldn't matter in this case, but _.has() answers the question, whereas boolean coercion only implies it. It's a better habit to check what you mean, particularly if people might be learning by example but not understanding the nuances: var o={a:false}; if (_.has(o,'a')) { log('It has an A'); } if (o.a) { log('It has an A?'); } else { log('Whoops!'); }
1418196282
Lithl
Pro
Sheet Author
API Scripter
I feel that boolean coercion to act as a null/undefined guard is a sufficiently common idiom in JS that it doesn't pose significant potential for confusion. You do have to be aware that actually being set to other falsey values can be problematic, but the variables used for guard conditions pretty much never, in my experience, have legitimate values that are falsey (unless you consider "I want to do XYZ" when mumble is undefined to make undefined a legitimate value for mumble). _.has has the advantage of performing a null guard on the parent object (so _.has(o, 'a') won't throw errors if o is null), but it doesn't guard against undefined, and since it's using hasOwnProperty it doesn't work with inherited properties.
1418218944
The Aaron
Forum Champion
API Scripter
Right. I use coercion all the time, just not when checking for the existence of a property. :)
1419896717

Edited 1419896757
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: This takes a status string and turns it into an object: statusmarkersToObject = function(stats) { return _.reduce(stats.split(/,/),function(memo,st){ var parts=st.split(/@/), num=parseInt(parts[1]||'0',10); if(parts[0].length) { memo[parts[0]]=Math.max(num,memo[parts[0]]||0); } return memo; },{}); }; I've written an inverse of this function, so that you can go back and forth between object and string: function objectToStatusmarkers(obj) { return _.map(obj, function(value, key) { return value &lt; 1 || value &gt; 9 ? key : key + '@' + parseInt(value); }) .join(','); }
1419911655
The Aaron
Forum Champion
API Scripter
Here are boiled down versions of what I use. Usually, I've done validate of the object ahead of time, so I don't need to do it as part of the string construction, but here's how I'd do it for a helper function. Note: You have to handle dead specially. If you give it a number, it will show up as a blank space in the status markers, not a red X on the token. var objectToStatusmarkers = function(obj) { 'use strict'; return _.map(obj,function(v,k){ return ( ('dead' === k) ? k : (k+'@'+(Math.min(9,Math.max(0,parseInt(v,10)||0)))) ); }).join(','); }; If you want to validate that you're only setting correct status names, here's a version that filters out anything that isn't appropriate using a regular expression. var objectToStatusmarkersValidated = function(obj) { 'use strict'; return _.chain(obj) .reduce(function(m,v,k){ if( (/^(?:red|blue|green|brown|purple|pink|yellow|dead|skull|sleepy|half-heart|half-haze|interdiction|snail|lightning-helix|spanner|chained-heart|chemical-bolt|death-zone|drink-me|edge-crack|ninja-mask|stopwatch|fishing-net|overdrive|strong|fist|padlock|three-leaves|fluffy-wing|pummeled|tread|arrowed|aura|back-pain|black-flag|bleeding-eye|bolt-shield|broken-heart|cobweb|broken-shield|flying-flag|radioactive|trophy|broken-skull|frozen-orb|rolling-bomb|white-tower|grab|screaming|grenade|sentry-gun|all-for-one|angel-outfit|archery-target)$/).test(k)) { m[k]=v; } return m; },{}) .map(function(v,k){ return ( ('dead' === k) ? k : (k+'@'+(Math.min(9,Math.max(0,parseInt(v,10)||0)))) ); }) .value() .join(','); };
1419918955
Lithl
Pro
Sheet Author
API Scripter
Good point on handling the dead status. Not sure I'd put in strict validation for all possible statuses, since last I checked nothing breaks if you use an invalid status, and the suggestion for custom statusmarkers has enough dev attention to at least get tagged [Long-Term] in the suggestions forum.
1419921132
The Aaron
Forum Champion
API Scripter
Sure. I'm just providing it since I had it laying about. =D
1419976144
Lithl
Pro
Sheet Author
API Scripter
Asynchronous Semaphore function Semaphore(callback, initial, context) { this.lock = initial || 0; this.callback = callback; this.context = context || callback; this.args = arguments.slice(3); } Semaphore.prototype = { v: function() { this.lock++; }, p: function() { var parameters; this.lock--; if (this.lock === 0 && this.callback) { // allow sem.p(arg1, arg2, ...) to override args passed to Semaphore constructor if (arguments.length &gt; 0) { parameters = arguments; } else { parameters = this.args; } this.callback.apply(context, parameters); } } }; Some of the methods provided by Roll20's API are asynchronous, meaning that they'll run when they run and they'll finish when they finish, get these kids off my lawn! Most notable among these is the sendChat function. Unfortunately, this means that you can't have some code, followed by sendChat, followed by additional code and expect sendChat to be done (especially if you passed a third parameter to sendChat as a callback). An asynchronous semaphore will let you run a function and ensure that all asynchronous operations (that it knows about) have completed. This implementation lets you supply a callback to run after all asynchronous operations, a number of planned asynchronous operations (although that number can increase), a context for the callback function, and parameters to pass to the callback. To construct the semaphore, you need to at least have a callback function for it to be useful; everything else is optional. var sem = new Semaphore(function() { log('Async operations complete!'); }); Before beginning each asynchronous operation, you need to call v() . Note that if you supply an appropriate number as the second parameter to the semaphore constructor, the verhogen (increase) function is not needed. At the end of each asynchronous operation, you need to call p() . (From prolaag , short for "try to reduce." These names come from Dutch.) var sem1 = new Semaphore(function() { log('sem1 processes complete'); }), sem2 = new Semaphore(function() { log('sem2 processes complete'); }, 2); sem1.v(); sendChat('', '/roll d20', function(ops) { sem1.p(); }); sem1.v(); sendChat('', '/roll d20', function(ops) { sem1.p(); }); sendChat('', '/roll d20', function(ops) { sem2.p(); }); sendChat('', '/roll d20', function(ops) { sem2.p(); }); This particular implementation of an asynchronous semaphore allows you to specify what "this" means within the callback, as well as pass parameters to the callback. If a context for the callback is not provided or is a falsey value (such as false or null ), the callback itself will be used as the context. The parameters for the callback can be specified in the constructor (simply add more parameters after the context) or in the p function call. If any parameters are given in p , parameters given in the constructor will be ignored. var sem = new Semaphore(function(lastAsync) { log(this === sem); log(lastAsync + ' completed last'); }, 2, sem, 'Sir not appearing in this callback'); sendChat('', '/roll d20', function(ops) { sem.p('First sendChat call'); }); sendChat('', '/roll d20', function(ops) { sem.p('Second sendChat call'); }); var sem = new Semaphore(function(undefinedParameter) { log(this === null); log(undefinedParameter === undefined); }, 1, null); sem.p();
1419977905
The Aaron
Forum Champion
API Scripter
Nice implementation. Probably the people for whom asynchronous functions cause problems won't understand semaphores either, but you never know. =D
1419978140

Edited 1419978370
Lithl
Pro
Sheet Author
API Scripter
Perhaps, but I hope the interface presented by the implementation is simple enough that anyone trying to use it is able to do so, while robust enough that anyone who knows what they're doing is happy. =) I'd love to add a queue to the semaphore, but that starts to get complicated, decisions between whether you fire elements of the queue at each prolaag or all elements in sequence or wait for a verhogen+prolaag, etc. A single callback semaphore in 22 lines (incl. a comment, empty lines, and single-closing-brace lines) is nice and compact.
1419978340
The Aaron
Forum Champion
API Scripter
=D