The roll structure is an array...
...of roll objects...
...that have properties like expression, rollid, and results... and the results is an object...
...with properties like resulttype, type, value, and rolls... where rolls is an array...
...of rolls...
...each of which has slightly different properties depending on the type of roll (R, M, C, etc.)
Sounds like a mess, right? Here are a few tools that can help.
First, if you want to rebuild the delving-of-the-roll yourself, you can look at the code of libInline to understand how the recursion works to unpack the roll object and then rebuild it. Are the roll components you want always going to be in the same position? Are your rolls always going to be built exactly the same way (ie, an outer roll closure with a single nested roll and exactly 4 roll terms)?
But you don't have to do the heavy lifting yourself. If you want the dice, you can use the functions of libInline to get the Dice based on their type (all, crit, fail, dropped, etc.). In other words, with libInline installed, you can pass a roll object (or all roll objects in the message) in, and then use a libInline function to return the dice you want. That is discussed in the libInline thread, with examples.
Finally, if you want to see more examples of inline rolls, you can use this debug script. It's a bit patchwork as I plug in components that I need, but it should work for what you're looking for. Though the script has other handles to help debug other facets of a game, the usage command you'd be looking for would be:
!debug-inline [[...inline roll to display here...]]
Drop an inline roll equation in there to see how Roll20 packages it up, and how libInline repackages it (it won't show the functions attached to the roll objects, of course).
Note: this WILL require libInline to be installed, too. It's available in the one-click, or you can copy it from the above link.
/*
=========================================================
Name : Debug
GitHub :
Roll20 Contact : timmaugh
Version : 0.0.3
Last Update : 8/25/2021
=========================================================
*/
var API_Meta = API_Meta || {};
API_Meta.Debug = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 };
{
try { throw new Error(''); } catch (e) { API_Meta.Debug.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); }
}
const Debug = (() => {
// ==================================================
// VERSION
// ==================================================
const apiproject = 'Debug';
API_Meta[apiproject].version = '0.0.3';
const vd = new Date(1629897260729);
const versionInfo = () => {
log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`);
return;
};
const logsig = () => {
// initialize shared namespace for all signed projects, if needed
state.torii = state.torii || {};
// initialize siglogged check, if needed
state.torii.siglogged = state.torii.siglogged || false;
state.torii.sigtime = state.torii.sigtime || Date.now() - 3001;
if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) {
const logsig = '\n' +
' _____________________________________________ ' + '\n' +
' )_________________________________________( ' + '\n' +
' )_____________________________________( ' + '\n' +
' ___| |_______________| |___ ' + '\n' +
' |___ _______________ ___| ' + '\n' +
' | | | | ' + '\n' +
' | | | | ' + '\n' +
' | | | | ' + '\n' +
' | | | | ' + '\n' +
' | | | | ' + '\n' +
'______________|_|_______________|_|_______________' + '\n' +
' ' + '\n';
log(`${logsig}`);
state.torii.siglogged = true;
state.torii.sigtime = Date.now();
}
return;
};
// ==================================================
// MESSAGING / CHAT REPORTING
// ==================================================
const HE = (() => {
const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g, '\\$1');
const e = (s) => `&${s};`;
const entities = {
'<': e('lt'),
'>': e('gt'),
"'": e('#39'),
'@': e('#64'),
'{': e('#123'),
'|': e('#124'),
'}': e('#125'),
'[': e('#91'),
']': e('#93'),
'"': e('quot'),
'*': e('#42')
};
const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`, 'g');
return (s) => s.replace(re, (c) => (entities[c] || c));
})();
const rowbg = ["#ffffff", "#dedede"];
const headerbg = {
normal: rowbg[1],
critical: "##F46065"
};
const msgtable = (bg, tablerows) => `<div style="width:100%;"><div style="border-radius:10px;border:2px solid #000000;background-color:${bg}; margin-right:16px; overflow:hidden;"><table style="width:100%; margin: 0 auto; border-collapse:collapse;font-size:12px;">${tablerows}</table></div></div>`;
const msg1header = (bg, colspan, cell1) => `<tr style="border-bottom:1px solid #000000;font-weight:bold;text-align:center; background-color:${bg}; line-height: 22px;"><td colspan = "${colspan}">${cell1}</td></tr>`;
const msg2header = (bg, cell1, cell2) => `<tr style="border-bottom:1px solid #000000;font-weight:bold;text-align:center; background-color:${bg}; line-height: 22px;"><td>${cell1}</td><td style="border-left:1px solid #000000;">${cell2}</td></tr>`;
const msg3header = (bg, cell1, cell2, cell3) => `<tr style="border-bottom:1px solid #000000;font-weight:bold;text-align:center; background-color:${bg}; line-height: 22px;"><td>${cell1}</td><td style="border-left:1px solid #000000;">${cell2}</td><td style="border-left:1px solid #000000;">${cell3}</td></tr>`;
const msg1row = (bg, rowcss, cell1) => `<tr style="background-color:${bg};"><td style="padding:4px;"><div style="${rowcss}">${cell1}</div></td></tr>`;
const msg2row = (bg, cell1, cell2) => `<tr style="background-color:${bg};font-weight:bold;"><td style="padding:1px 4px;">${cell1}</td><td style="border-left:1px solid #000000;text-align:center;padding:1px 4px;font-weight:normal;">${cell2}</td></tr>`;
const msg3row = (bg, cell1, cell2, cell3) => `<tr style="background-color:${bg};font-weight:bold;"><td style="padding:1px 4px;">${cell1}</td><td style="border-left:1px solid #000000;text-align:center;padding:1px 4px;font-weight:normal;">${cell2}</td><td style="border-left:1px solid #000000;text-align:center;padding:1px 4px;font-weight:normal;">${cell3}</td></tr>`;
const msgbox = ({ c: c = "chat message", t: t = "title", btn: b = "buttons", send: send = true, sendas: sas = "API", wto: wto = "", type: type = "normal" }) => {
let hdr = msg1header(headerbg[type], '1', t);
let row = msg1row(rowbg[0], '', c);
let btn = b !== 'buttons' ? msg1row(rowbg[0], 'text-align:right;margin:4px 4px 8px;', b) : '';
let msg = msgtable(rowbg[0], hdr + row + btn);
if (wto) msg = `/w "${wto}" ${msg}`;
if (["t", "true", "y", "yes", true].includes(send)) {
sendChat(sas, msg);
} else {
return msg;
}
};
const replacer = (key, value) => {
// Filtering out properties
if (key === 'signature') {
return undefined;
}
return value;
};
const syntaxHighlight = (str, replacer = undefined) => {
const css = {
stringstyle: 'mediumblue;',
numberstyle: 'magenta;',
booleanstyle: 'darkorange;',
nullstyle: 'darkred;',
keystyle: 'darkgreen;'
};
if (typeof str !== 'string') {
str = JSON.stringify(str, replacer, ' ');
}
str = str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return str.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function (match) {
let cls = 'numberstyle';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'keystyle';
} else {
cls = 'stringstyle';
}
} else if (/true|false/.test(match)) {
cls = 'booleanstyle';
} else if (/null/.test(match)) {
cls = 'nullstyle';
}
return '<span style=" color: ' + css[cls] + '">' + HE(match.replace(/^"(.*)"(:?)$/g, ((m, g1, g2) => `${g1}${g2}`)).replace(/\\(.)/g, `$1`)) + '</span>';
});
};
const showObjInfo = (o, t = 'PARSED OBJECT', replacer = undefined) => {
msgbox({ t: t, c: `<div><pre style="background: transparent; border: none;white-space: pre-wrap;font-family: Inconsolata, Consolas, monospace;">${syntaxHighlight(o || '', replacer).replace(/\n/g, '<br>')}</pre></div>`, send: true });
return;
};
// ==================================================
// UTILITIES
// ==================================================
const conditionalPluck = (array, key, cobj = {}) => {
// test array of objects to return a given property of each object if all conditions are met
// cobj properties are functions testing that property (k) in the evaluated object (o)
// to test if testedproperty equals a given value: { testedProperty: (k,o) => { return o[k] === 'given value'; } }
// to test if testedproperty exists: { testedProperty: (k,o) => { return o.hasOwnProperty(k); } }
return array.map(o => {
let b = true;
if (cobj) {
Object.keys(cobj).forEach(k => {
if (b && !cobj[k](k, o)) {
b = false;
}
});
}
if (b) return o[key];
}).filter(e => e);
};
const simpleObj = (o) => JSON.parse(JSON.stringify(o));
const charFromAmbig = (info) => { // find a character where info is an identifying piece of information (id, name, or token id)
let character;
character = findObjs({ type: 'character', id: info })[0] ||
findObjs({ type: 'character' }).filter(c => c.get('name') === info)[0] ||
findObjs({ type: 'character', id: (getObj("graphic", info) || { get: () => { return "" } }).get("represents") })[0];
return character;
};
const getTheSpeaker = msg => {
let speaking;
if (msg.who === 'API') {
speaking = { id: undefined, type: 'API', localName: 'API', speakerType: 'API', chatSpeaker: 'API', get: (p) => { return 'API'; } };
} else {
let characters = findObjs({ type: 'character' });
characters.forEach(c => { if (c.get('name') === msg.who) speaking = c; });
if (speaking) {
speaking.speakerType = "character";
speaking.localName = speaking.get("name");
} else {
speaking = getObj('player', msg.playerid);
speaking.speakerType = "player";
speaking.localName = speaking.get("displayname");
//speaking.get = (p) => {
// switch (p) {
// case 'name':
// return speaking.localName;
// default:
// log(`Unknown property requested from a player speaker: ${p}`);
// return '';
// }
// }
}
speaking.chatSpeaker = speaking.speakerType + '|' + speaking.id;
}
return speaking;
};
const repeatingOrdinal = (character_id, section = '', attr_name = '') => {
if (!section && !attr_name) return;
let ordrx, match;
if (attr_name) {
ordrx = /^repeating_([^_]+)_([^_]+)_.*$/;
if (!ordrx.test(attr_name)) return; // the supplied attribute name isn't a repeating attribute at all
match = ordrx.exec(attr_name);
section = match[1];
}
let sectionrx = new RegExp(`repeating_${section}_([^_]+)_.*$`);
let createOrderKeys = [...new Set(findObjs({ type: 'attribute', characterid: character_id })
.filter(a => sectionrx.test(a.get('name')))
.map(a => sectionrx.exec(a.get('name'))[1]))];
let sortOrderKeys = (findObjs({ type: 'attribute', characterid: character_id, name: `_reporder_repeating_${section}` })[0] || { get: () => { return ''; } })
.get('current')
.split(/\s*,\s*/)
.filter(a => createOrderKeys.includes(a));
sortOrderKeys.push(...createOrderKeys.filter(a => !sortOrderKeys.includes(a)));
return attr_name ? sortOrderKeys.indexOf(match[2]) : sortOrderKeys;
};
const testRptOrdinal = msg => {
let [char, section, attr_name] = msg.content.split(' ').slice(1);
char = charFromAmbig(char);
let repset = repeatingOrdinal(char.id, section, attr_name);
let sectiontable = msgtable.replace("__bg__", rowbg[0]),
sectionheader = msg1header.replace("__colspan__", '2').replace("__bg__", rowbg[1]).replace("__cell1__", `RPTG ITEM LIST`) + msg2header.replace("__bg__", rowbg[1]).replace("__cell1__", "INDEX").replace("__cell2__", "ID");
let attrrows = repset.reduce((m, v, i) => {
return m + msg2row.replace("__bg__", rowbg[(i % 2)]).replace("__cell1__", i).replace("__cell2__", v);
}, sectionheader);
sendChat('API', sectiontable.replace("__TABLE-ROWS__", attrrows));
};
const testInlineRolls = msg => {
if (_.has(msg, 'inlinerolls')) {
showObjInfo(msg.inlinerolls, 'INLINE ROLL ORIGINAL', replacer);
let ird = libInline.getRollData(msg);
showObjInfo(ird, 'INLINE ROLL PARSED');
sendChat('API', `<div>${msg.content.replace(/\$\[\[(\d+)]]/g, ((m, g1) => ird[g1].getRollTip()))}</div>`);
}
};
const launcher = msg => {
sendChat('', `!debug-receiver${/\s+/.test(msg.content) ? msg.content.slice(msg.content.indexOf(' ')) : ''}`);
return;
};
const launch = msg => {
if (/\s./.test(msg.content)) {
sendChat('', `!${msg.content.slice(msg.content.indexOf(' ') + 1)}`);
}
}
const receiver = msg => {
let report = { content: msg.content, selected: msg.selected, who: msg.who, playerid: msg.playerid };
showObjInfo(msg, 'RECEIVER REPORT');
return;
};
const showstate = msg => {
if (!/\s/.test(msg.content)) {
showObjInfo(state, `state`);
} else {
let treekeys = msg.content.slice(msg.content.indexOf(' ') + 1).split('.');
let o = treekeys.reduce((m, v) => {
m = _.clone(m[v]) || undefined;
return m;
}, state);
showObjInfo(o, `state${treekeys.length ? '.' : ''}${treekeys.join('.')}`);
}
return;
};
const deleteStatePart = msg => {
if (!/\s+./.test(msg.content)) {
msgbox({ t: 'THIS IS FOR YOUR OWN GOOD', c: `You can't delete your state object.`, send: true, type: 'critical' })
return;
} else {
let treekeys = msg.content.slice(msg.content.indexOf(' ') + 1).split('.');
let deletekey = treekeys.pop();
let o = treekeys.reduce((m, v) => {
let k = m[v] || undefined;
return k;
}, state);
delete o[deletekey];
showObjInfo(o, `state${treekeys.length ? '.' : ''}${treekeys.join('.')}`);
}
return;
};
const aboutme = msg => {
let title = '';
if (!/\s+./.test(msg.content)) {
showObjInfo(simpleObj(msg), 'ABOUT THE MESSAGE OBJECT');
return;
} else {
let oarray = msg.content.split(/\s+--/).slice(1);
oarray.forEach(o => {
let gameobj, otype, oname;
if (/\|/.test(o)) {
[otype, oname] = o.split('|');
gameobj = findObjs({ type: otype }).filter(reto => reto.get('name') === oname)[0];
} else {
gameobj = findObjs({ id: o === 'speaker' ? getTheSpeaker(msg).id : o })[0];
}
if (gameobj) {
title = `ABOUT ${(gameobj.type || gameobj._type || gameobj.get('type')).toUpperCase()}: ${gameobj.id}`;
showObjInfo(simpleObj(gameobj), title);
return;
} else {
msgbox({ t: 'NO OBJECT FOR ID', c: `No object found for ID: ${o}`, send: true });
return;
}
});
}
};
const handleInput = (msg) => {
if (msg.type !== 'api') return;
if (/^!debug-rptord/.test(msg.content)) {
testRptOrdinal(msg);
} else if (/^!debug-inline/.test(msg.content)) {
testInlineRolls(msg);
} else if (/^!debug-launcher/.test(msg.content)) {
launcher(msg);
return;
} else if (/^!debug-launch/.test(msg.content)) {
launch(msg);
} else if (/^!debug-receiver/.test(msg.content)) {
receiver(msg);
return;
} else if (/^!debug-showstate/.test(msg.content)) {
showstate(msg);
return;
} else if (/^!debug-delstate/.test(msg.content)) {
deleteStatePart(msg);
} else if (/^!debug-aboutme/.test(msg.content)) {
aboutme(msg);
}
};
regHandlers = () => {
on('chat:message', handleInput);
};
on('ready', () => {
versionInfo();
logsig();
regHandlers();
setTimeout(() => log(`--------------------------------------------------------`), 2000);
});
return {
ShowObjInfo: showObjInfo
};
})();
{ try { throw new Error(''); } catch (e) { API_Meta.Debug.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Debug.offset); } }