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

Recursive Table - API commands

I'm feeling very dense today. Should Recursive Table automatically parse API commands that are items in a table? I'm not able to get it to parse the commands. For reference: I want to make a table of Hellish Rebuke insults that use the Tongues API script to convert to Infernal. My table items are : !tongues Infernal Burn etc But when I try calling them via a normal table call I just get the API command and not the output. I thought Recursive Table might be the answer but it doesn't seem to help.
1558378472
The Aaron
Roll20 Production Team
API Scripter
Hmm. Try: !rt !tongues Infernal Burn and see if that works. If it does, you can adjust your table to just have the words and not the !tongues command: !rt !tongues [[1t[vicious_mockery]]] If it doesn't, I'll need to see if I can change RecursiveTable to allow it to work. 
The Aaron said: Hmm. Try: !rt !tongues Infernal Burn and see if that works. If it does, you can adjust your table to just have the words and not the !tongues command: !rt !tongues [[1t[vicious_mockery]]] If it doesn't, I'll need to see if I can change RecursiveTable to allow it to work.  Nothing happens (like no output at all) when I try: !rt !tongues Infernal burn
1558385052
The Aaron
Roll20 Production Team
API Scripter
Hmm. That's probably because tongues relies on the msg.playerid in order to translate the message, and the id from the API will just be "api".  I'll have to see what I can do. 
The Aaron said: Hmm. That's probably because tongues relies on the msg.playerid in order to translate the message, and the id from the API will just be "api".  I'll have to see what I can do.  I wouldn't worry about it too much. It was just an idea. I'll figure something else out!
1558404735

Edited 1558573122
The Aaron
Roll20 Production Team
API Scripter
Try this version of Tongues, I adjusted it to expand inline rolls, so this should work: !tongues [some language] [some inline roll] like !tongues Draconic [[1t[vicious_mockery]]] or, if you have the language on the start of the row, this should work: !tongues [[1t[vicious_mocker]]] code: // Fix for HTML and inline rolls by The Aaron var Tongues = Tongues || (function(){ 'use strict'; //---- INFO ----// var script = { name: 'Tongues', version: '4.5.3'}, devMode = false, languages = {}, //---- PRIVATE FUNCTIONS ----// startup = function(){ if (!state.Tongues || devMode){ resetState(); } var handouts = findObjs({ _type: 'handout' }, {caseInsensitive: true}); _.each(handouts, function(handout){ loadHandout(handout, false); }); log('> ' + script.name + ' (v' + script.version + '): Found ' + Object.keys(languages).length + ' languages <'); }, resetState = function(){ state.Tongues = { savedSpeaker: 'John Smith', showTokenName: false, displayLanguageName: true, version: script.version }; log('> ' + script.name + ' (v' + script.version + ') created new state storage <'); }, loadHandout = function(handout, reload=true){ const stripHTML = (t) => t.replace(/<br\/?>/gi,'\n').replace(/<\/div>/gi,'\n').replace(/<[^>]*?>/g,''); var handoutName = handout.get('name'); if (handoutName.startsWith('Tongues: ')){ var languageName = handoutName.replace('Tongues: ', ''); if (!languages[languageName]){ languages[languageName] = {}; } languages[languageName].obj = handout; if (languages[languageName].obj){ languages[languageName].obj.get('notes', function(notes){ notes = stripHTML(notes); if (textIsValid(notes)){ dLog(languageName + ' is valid!'); var matches = notes.match(/(\[.*?\])/igm); languages[languageName].dictionary = {}; languages[languageName].vocabulary = []; for (var i = 0; i < matches.length; i++){ if(matches[i].match(/(\[.+:.+\])/igm)){ matches[i] = matches[i].replace(/\[([^[\]]+)\]/ig, '$1'); var words = matches[i].toLowerCase().split(/\s*:\s*/); var originals = words[0].split(/\s*,\s*/); var translation = words[1]; _.each(originals, function(original){ languages[languageName].dictionary[original.trim()] = translation.trim(); }); } else if(matches[i] == '[]'){ languages[languageName].vocabulary[i] = null; } else { matches[i] = matches[i].replace(/\[([^[\]]+)\]/ig, '$1'); languages[languageName].vocabulary[i] = matches[i].split(/\s*,\s*/); } } if (reload){ log('> ' + script.name + ' (v' + script.version + '): ' + languageName + ' parsed succesfully! (' + Object.keys(languages).length + ' languages loaded) <'); } languages[languageName].obj.get('gmnotes', function(gmnotes){ gmnotes = stripHTML(gmnotes); if (textIsValid(gmnotes)){ dLog(languageName + ' speakers are valid!'); languages[languageName].speakers = []; var languageSpeakers = gmnotes.replace(/<.*?>/g,'').split(/\s*,\s*/); dLog(languageName + ' speakers: ' + languageSpeakers); _.each(languageSpeakers, function(languageSpeaker){ languageSpeaker = languageSpeaker.split(/\s*:\s*/); if (languageSpeaker[1]){ languages[languageName].speakers.push({ name: languageSpeaker[0].trim(), learning: parseInt(languageSpeaker[1].trim()) }); } else { languages[languageName].speakers.push({ name: languageSpeaker[0].trim(), learning: 100 }); } }); } }); } }); } else { delete languages[languageName]; log('> ' + script.name + ' (v' + script.version + '): ' + languageName + ' has a wrong format and cannot be parsed! (' + Object.keys(languages).length + ' languages loaded) <'); } } else { _.each(Object.keys(languages), function(key){ if(languages[key].obj.get('id') === handout.get('id')){ delete languages[key]; log('> ' + script.name + ' (v' + script.version + '): ' + key + ' removed successfully! (' + Object.keys(languages).length + ' languages loaded) <'); } }); } return; }, deleteHandout = function(handout){ var handoutName = handout.get('name'); if (handoutName.startsWith('Tongues: ')){ var languageName = handoutName.replace('Tongues: ', ''); delete languages[languageName]; log('> ' + script.name + ' (v' + script.version + '): ' + languageName + ' removed successfully! (' + Object.keys(languages).length + ' languages loaded) <'); } }, commandConfig = function(msg, command){ var param = command[3].split(/\s*:\s*/); var bool = (param[1] === 'true'); switch (param[0]){ case 'show': var text = '<br>'; text += '[savedSpeaker] ' + state.Tongues.savedSpeaker + '<br>'; text += '[showTokenName] ' + state.Tongues.showTokenName + '<br>'; text += '[displayLanguageName] ' + state.Tongues.displayLanguageName + '<br>'; sendChat('Tongues', '/w ' + msg.who + text, null, {noarchive:true}); break; case 'reset': resetState(); sendChat('Tongues', '/w ' + msg.who + 'Configuration successfully restored to default!', null, {noarchive:true}); break; case 'displayLanguageName': state.Tongues.displayLanguageName = bool; sendChat('Tongues', '/w ' + msg.who + '<br>[displayLanguageName]<br> Successfully changed to ' + bool + '!', null, {noarchive:true}); break; case 'showTokenName': state.Tongues.showTokenName = bool; sendChat('Tongues', '/w ' + msg.who + '<br>[showTokenName]<br> Successfully changed to ' + bool + '!', null, {noarchive:true}); break; default: sendChat('Tongues', '/w ' + msg.who + ' That configuration parameter does not exist!', null, {noarchive:true}); break; } }, commandAbility = function(msg, command, info=true){ if (!msg.selected){ sendChat('Tongues', '/w ' + msg.who + ' You must select a character token!', null, {noarchive:true}); } else if (msg.selected.length > 1){ sendChat('Tongues', '/w ' + msg.who + ' You must select only one character token!', null, {noarchive:true}); } else { var token = getObj('graphic', msg.selected[0]._id); var speakerId = token.get('represents'); var macro = '!tongues ?{Select a language'; _.each(Object.keys(languages), function(key){ if(isLanguageSpeaker(speakerId, languages[key].speakers)){ macro += '|' + key; } }); macro += '} ?{Message}'; var abilities = findObjs({ name: 'Tongues', characterid: speakerId }); var message = ''; if (abilities && abilities.length > 0){ abilities[0].set({ action: macro }); message = ' Ability successfully updated!'; } else { createObj("ability", { name: 'Tongues', characterid: speakerId, action: macro, istokenaction: true }); message = ' Ability successfully created!'; } if (info) { sendChat('Tongues', '/w ' + msg.who + message, null, {noarchive:true}); } } }, commandSet = function(msg, command, unset=false){ if (!msg.selected){ sendChat('Tongues', '/w ' + msg.who + ' You must select a character token!', null, {noarchive:true}); } else if (msg.selected.length > 1){ sendChat('Tongues', '/w ' + msg.who + ' You must select only one character token!', null, {noarchive:true}); } else { var token = getObj('graphic', msg.selected[0]._id); var character = getObj('character', token.get('represents')); var params = command[3].split(/\s*:\s*/); var languageName = params[0].trim(); var languageLearning = undefined; if (params[1]) { languageLearning = parseInt(params[1].trim()); } if (languages[languageName]){ languages[languageName].obj.get('gmnotes', function(gmnotes){ if (textIsValid(gmnotes)){ var speakers = gmnotes.split(/\s*,\s*/); /* let speaker = */ _.find(speakers, function(speaker){ speaker = speaker.trim().split(/\s*:\s*/); if(speaker[0] == character.get('name')){ var pattern = new RegExp('[, ]*' + speaker[0], 'igm'); if (speaker[1]){ pattern = new RegExp('[, ]*' + speaker[0] + ':' + speaker[1], 'igm'); } gmnotes = gmnotes.replace(pattern, ''); } }); } else { gmnotes = ''; } var text = ((gmnotes)? gmnotes + ', ':'') + character.get('name'); if(languageLearning){ text += ':' + languageLearning + '%'; } if(!unset){ languages[languageName].obj.set({ gmnotes: text }); sendChat('Tongues', '/w ' + msg.who + ' Language knowledge succesfully added!', null, {noarchive:true}); } else { languages[languageName].obj.set({ gmnotes: gmnotes }); sendChat('Tongues', '/w ' + msg.who + ' Language knowledge succesfully removed!', null, {noarchive:true}); } var abilities = findObjs({ name: 'Tongues', characterid: character.get('_id') }); if (abilities && abilities.length > 0){ msg.content = '!tongues --ability'; commandAbility(msg, ['!tongues', '--ability', null], false); } }); } else { sendChat('Tongues', '/w ' + msg.who + ' ' + languageName + ' does not exist as a Tongues valid language!', null, {noarchive:true}); } } }, commandCreate = function(msg, command){ var newLanguage = createObj('handout', { name: 'Tongues: ' + command[3] }); var text = 'Vocabulary: <br>'; for(var i = 0; i < 14; i++){ text += command[3] + ' ' + (i+1) + '-letter words: [] <br>'; } text += '<hr>Dictionary: <br>[Language, Speech, Cant, Dialect: Tongue]<br>[Hello: Goodbye]<br>[Goodbye: Hello]'; newLanguage.set({ notes: text }); sendChat('Tongues', '/w ' + msg.who + ' ' + command[3] + ' language template succesfully created!', null, {noarchive:true}); }, commandSpeaker = function(msg, command){ state.Tongues.savedSpeaker = command[3]; sendChat('Tongues', '/w ' + msg.who + 'Default speaker name successfully changed!', null, {noarchive:true}); }, commandSpeak = function(msg, command){ if (!msg.selected && !playerIsGM(msg.playerid)){ sendChat('Tongues', '/w ' + msg.who + ' You must select a character token!', null, {noarchive:true}); } else if (msg.selected && msg.selected.length > 1){ sendChat('Tongues', '/w ' + msg.who + ' You must select only one character token!', null, {noarchive:true}); } else { var token = (msg.selected)?getObj('graphic', msg.selected[0]._id):null; var speakerId = (token)?token.get('represents'):'#' + state.Tongues.savedSpeaker; if (!speakerId || speakerId == ''){ speakerId = '#' + getTokenName(token); } var languageName = command[2]; var text = command[3]; if (languages[languageName] && languages[languageName].obj){ if (languages[languageName].speakers){ var learning = isLanguageSpeaker(speakerId, languages[languageName].speakers); if (learning){ translate(token, speakerId, languageName, text, learning); } else if (!learning && playerIsGM(msg.playerid)) { translate(token, speakerId, languageName, text, 100); } else { sendChat('Tongues', '/w ' + msg.who + ' That character cannot speak ' + languageName + '!', null, {noarchive:true}); } } else if (playerIsGM(msg.playerid)) { translate(token, speakerId, languageName, text, 100, false); } else { sendChat('Tongues', '/w ' + msg.who + ' There are no speakers of ' + languageName + '!', null, {noarchive:true}); } } else { sendChat('Tongues', '/w ' + msg.who + ' ' + languageName + ' does not exist as a Tongues valid language!', null, {noarchive:true}); } } }, translate = function(token, speakerId, languageName, text, learning, translate = true){ _.each(Object.keys(languages[languageName].dictionary), function(key){ var pattern = new RegExp(key, 'ig'); text = text.replace(pattern, function(word){ return matchCase(key.replace(/\s/ig, '_'), word); }); }); var translatedText = text.replace(/\$[^$]+\$|([\u00BF-\u1FFF\u2C00-\uD7FF\w_]+)/igm, function(word){ word = word.replace(/_/ig, ' '); //NOT TRANSLATED if(word.match(/\$[^$]+\$/ig)){ return word.replace(/\$/ig, ''); } //NOT ENOUGH LEARNING TO TRANSLATE var difficulty = (wordHash(word) % 100) + 1; if (learning < difficulty){ return word; } //TRANLSATED WITH DICTIONARY if(languages[languageName].dictionary[word.toLowerCase()]){ return matchCase(languages[languageName].dictionary[word.toLowerCase()], word); } //TRANSLATED WITH VOCABULARY var n = 1; while (!languages[languageName].vocabulary[word.length - n] && word.length - n >= 0){ n++; } if (word.length - n < 0){ return word.replace(/[\u00BF-\u1FFF\u2C00-\uD7FF\w]/ig, '?'); } else { var hash = wordHash(word) % languages[languageName].vocabulary[word.length - n].length; return matchCase(languages[languageName].vocabulary[word.length - n][hash], word); } }); var originalText = text.replace(/\$[^$]+\$|([\u00BF-\u1FFF\u2C00-\uD7FF\w_]+)/igm, function(word){ word = word.replace(/_/ig, ' '); //NOT TRANSLATED if(word.match(/\$[^$]+\$/ig)){ return word.replace(/\$([^$]+)\$/ig, '<span style="color: RoyalBlue;">$1</span>'); } //NOT ENOUGH LEARNING TO TRANSLATE var difficulty = (wordHash(word) % 100) + 1; if (learning < difficulty){ return '<span style="color: RoyalBlue;">' + word + '</span>'; } //TRANSLATED return word; }); var speaker; if (state.Tongues.showTokenName){ speaker = getTokenName(token); } else if (speakerId.startsWith('#')){ speaker = speakerId.replace('#', ''); } else { speaker = 'character|' + speakerId; } if (state.Tongues.displayLanguageName){ sendChat(speaker, '[' + languageName + '] ' + translatedText); } else { sendChat(speaker, translatedText); } sendChat('Tongues - GM', '/w GM' + ' [' + languageName + '] ' + originalText); if (translate){ _.each(languages[languageName].speakers, function(speaker){ var speakerObj = findObjs({ _type: 'character', name: speaker.name }, {caseInsensitive: true})[0]; if (speakerObj && speakerObj.get('controlledby')){ if (speaker.learning >= 100){ sendChat('Tongues [' + speakerObj.get('name') + ']', '/w ' + speakerObj.get('name') + ' [' + languageName + '] ' + originalText); } else { var understoodText = text.replace(/\$[^$]+\$|([\u00BF-\u1FFF\u2C00-\uD7FF\w_]+)/igm, function(word){ word = word.replace(/_/ig, ' '); //NOT TRANSLATED if(word.match(/\$[^$]+\$/ig)){ return word.replace(/\$([^$]+)\$/ig, '<span style="color: RoyalBlue;">$1</span>'); } //NOT ENOUGH LEARNING TO UNDERSTAND var difficulty = (wordHash(word) % 100) + 1; if (learning < difficulty){ return '<span style="color: RoyalBlue;">' + word + '</span>'; } //NOT ENOUGH LEARNING TO UNDERSTAND if (speaker.learning < difficulty){ //TRANLSATED WITH DICTIONARY if(languages[languageName].dictionary[word.toLowerCase()]){ return '<span style="color: FireBrick;">' + matchCase(languages[languageName].dictionary[word.toLowerCase()], word) + '</span>'; } //TRANSLATED WITH VOCABULARY var n = 1; while (!languages[languageName].vocabulary[word.length - n] && word.length - n >= 0){ n++; } if (word.length - n < 0){ return word.replace(/[\u00BF-\u1FFF\u2C00-\uD7FF\w]/ig, '?'); } else { var hash = wordHash(word) % languages[languageName].vocabulary[word.length - n].length; return '<span style="color: FireBrick;">' + matchCase(languages[languageName].vocabulary[word.length - n][hash], word) + '</span>'; } } //TRANSLATED return word; }); sendChat('Tongues', '/w ' + speakerObj.get('name') + ' [' + languageName + '] ' + understoodText); } } }); } }, isLanguageSpeaker = function(speakerId, speakers) { if(speakerId.startsWith('#')){ return false; } var character = getObj('character', speakerId); var learning = null; if (character){ var characterName = character.get('name'); _.find(speakers, function(speaker) { if (characterName == speaker.name){ learning = speaker.learning; return true; } }); } return learning; }, wordHash = function(str){ var hash = 0, i, chr; if (str.length === 0) return hash; for (i = 0; i < str.length; i++) { chr = str.charAt(i).toLowerCase().charCodeAt(0); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } return Math.abs(hash); }, matchCase = function(text, pattern) { var result = ''; for(var i = 0; i < text.length; i++) { var c = text.charAt(i); var p = pattern.charCodeAt(i); if(p >= 65 && p < 65 + 26) { result += c.toUpperCase(); } else { result += c.toLowerCase(); } } return result; }, getTokenName = function(token) { var name = token.get('name'); if (!name || name == ''){ name = state.Tongues.savedSpeaker; } return name; }, textIsValid = function(string) { return ( string && string != '' && string != 'null' && !string.match(/^<.*?>/g) ); }, //----- INPUT HANDLER -----// handleInput = function(msg){ const processInlinerolls = (msg) => { if(_.has(msg,'inlinerolls')){ return _.chain(msg.inlinerolls) .reduce(function(m,v,k){ let ti=_.reduce(v.results.rolls,function(m2,v2){ if(_.has(v2,'table')){ m2.push(_.reduce(v2.results,function(m3,v3){ m3.push(v3.tableItem.name); return m3; },[]).join(', ')); } return m2; },[]).join(', '); m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } else { return msg.content; } }; if (msg.type == 'api' && msg.content.startsWith('!tongues ')){ var regex = /(![^ ]+) ([^ ]+)(.+)*/igm; var command = regex.exec(processInlinerolls(msg)); if (command[3]){ command[3] = command[3].trim(); } if (command && command[2]){ if(command[2].startsWith("--")){ if(playerIsGM(msg.playerid)){ if(command[2] == '--create' && command[3]){ commandCreate(msg, command); } else if (command[2] == '--config' && command[3]) { commandConfig(msg, command); } else if (command[2] == '--ability') { commandAbility(msg, command); } else if (command[2] == '--set' && command[3]) { commandSet(msg, command); } else if (command[2] == '--unset' && command[3]) { commandSet(msg, command, true); } else if (command[2] == '--speaker' && command[3]) { commandSpeaker(msg, command); } else { sendChat('Tongues', '/w ' + msg.who + ' Invalid command!', null, {noarchive:true}); } } else { sendChat('Tongues', '/w ' + msg.who + ' Only the GM can access Tongues configuration commands!', null, {noarchive:true}); } } else if (command[3]) { commandSpeak(msg, command); } else { sendChat('Tongues', '/w ' + msg.who + ' Invalid command!', null, {noarchive:true}); } } else { sendChat('Tongues', '/w ' + msg.who + ' Invalid command!', null, {noarchive:true}); } } }, //---- PUBLIC FUNCTIONS ----// dLog = function(message){ if (devMode) log(message); }, registerEventHandlers = function(){ on('chat:message', handleInput); on('change:handout', loadHandout); on('destroy:handout', deleteHandout); }, checkInstall = function(){ log('> ' + script.name + ' (v' + script.version + ') is installed and running <'); startup(); }; return { checkInstall: checkInstall, registerEventHandlers: registerEventHandlers }; }()); on("ready", function() { Tongues.checkInstall(); Tongues.registerEventHandlers(); });
I copy-pasted the changed script into my test game but it seems to be crashing the sandbox: For reference, the error message generated was:  TypeError: Cannot convert undefined or null to object TypeError: Cannot convert undefined or null to object at Function.keys (<anonymous>) at translate (apiscript.js:12746:23) at commandSpeak (apiscript.js:12736:21) at handleInput (apiscript.js:12988:21) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1634:12 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) TypeError: Cannot convert undefined or null to object at Function.keys (<anonymous>) at translate (apiscript.js:12746:23) at commandSpeak (apiscript.js:12736:21) at handleInput (apiscript.js:12988:21) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1634:12 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) I have a table "Rebuke" with a single item: Burn! My command was: !tongues Infernal [[1t[Rebuke]]]
1558467045
The Aaron
Roll20 Production Team
API Scripter
Hmm.. that seems to indicate a crash on this line: translate = function(token, speakerId, languageName, text, learning, translate = true){ _.each(Object.keys(languages[languageName].dictionary), function(key){ To be honest, I've never touched this script before and I didn't learn how to set it up.  If you want to PM me an invite to your game and GM me, I can probably track that down in short order and fix it for you.
The Aaron said: Hmm.. that seems to indicate a crash on this line: translate = function(token, speakerId, languageName, text, learning, translate = true){ _.each(Object.keys(languages[languageName].dictionary), function(key){ To be honest, I've never touched this script before and I didn't learn how to set it up.  If you want to PM me an invite to your game and GM me, I can probably track that down in short order and fix it for you. Mr The Aaron, do you honestly enjoy logging into other people's games to sort out their API woes? Or do you do it out of a sick sense of responsibility due to your status as the ultimate API guru? :D were-not-worthy.gif ...I PM'd you a link.
1558543451
The Aaron
Roll20 Production Team
API Scripter
I enjoy it. =D
1558572937
The Aaron
Roll20 Production Team
API Scripter
Ok, resolved! It turns out, this is an existing problem with the Tongues script.&nbsp; It was introduced with the Text Editor changes that caused everything to be HTML instead of plain text.&nbsp; I have fixed the script above for future Tongues users. Others with this problem, whom I'll be PMing shortly. =D <a href="https://app.roll20.net/forum/post/6603487/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6603487/slug%7D</a> <a href="https://app.roll20.net/forum/post/7444930/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/7444930/slug%7D</a> <a href="https://app.roll20.net/forum/post/7191143/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/7191143/slug%7D</a>
The Aaron said: Ok, resolved! It turns out, this is an existing problem with the Tongues script.&nbsp; It was introduced with the Text Editor changes that caused everything to be HTML instead of plain text.&nbsp; I have fixed the script above for future Tongues users. Others with this problem, whom I'll be PMing shortly. =D <a href="https://app.roll20.net/forum/post/6603487/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6603487/slug%7D</a> <a href="https://app.roll20.net/forum/post/7444930/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/7444930/slug%7D</a> <a href="https://app.roll20.net/forum/post/7191143/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/7191143/slug%7D</a> You are the best!
1558584253
The Aaron
Roll20 Production Team
API Scripter
=D