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

3D dice not triggered from Macro/Script

Hello, I'm new to Roll20 scripting and am having trouble getting the 3D dice to generate from a macro that calls a script.

The macro is just calling !3dRoll with no parameters.

The (short) script is at: https://github.com/fnord12/3dRoll/blob/main/3dRoll.js

The relevant line is:

sendChat("Trialman", "/r [[3d6]]", null, {use3d: true});


The script works in terms of generating the values of the D6s and posting them in the chat.  But it won't trigger the 3D animated die rolls.  I saw a forum post from a year ago saying that this feature was broken.  Could that still be the case?  Or is something else in my script stopping the animation?  Thanks!

April 11 (3 years ago)
Oosh
Sheet Author
API Scripter

AFAIK it's still not possible to get 3d dice working from an API script. It seems to be hard coded to disable 3d dice internally when the call comes from the API - presumably that was done because Something Bad was happening, at a guess the API was able to potentially overload the Quantum server in a way that players can't.

April 11 (3 years ago)

Edited April 11 (3 years ago)

Thanks Oosh.  I've put a note on the wiki.

April 11 (3 years ago)
Ulti
Pro
Sheet Author
API Scripter

There is a suggestion to allow API to actually roll 3D dices.

April 11 (3 years ago)
The Aaron
Roll20 Production Team
API Scripter

You can actually get it to work, but it requires some more steps. 

If you look at my Tagmar script, line 300 is where it kicks this off. To get it to work, you have to send the message to chat, and catch it as another api chat command. You can have the actual message be hidden (that's what I do in Tagmar), then pick things up where you left off. This does add a bit of latency to the execution, and you need to make use of the Memento Pattern to maintain your execution state across API calls, but it is effective. 

April 12 (3 years ago)

Edited April 12 (3 years ago)
Oosh
Sheet Author
API Scripter

When was the last time you tested that, Aaron? Tagmar also has no 3d dice when I test it.


Just to clarify - it isn't the callback stopping the 3d dice. Any sendChat from the API will fail, as far as I can tell.

April 12 (3 years ago)

Edited April 12 (3 years ago)
The Aaron
Roll20 Production Team
API Scripter

Well dang, they broke it!  

I sent them a note and repro steps.

April 13 (3 years ago)
Oosh
Sheet Author
API Scripter

You could get it to work by just rolling normally and reacting to that. Come up with a trigger word that won't appear anywhere except your macro, and slap a display:none in there:

[[[1d20+5]]myTriggerKey](#" style="display:none)


Then wait for your trigger to come up and do stuff:

on('ready', () => {
  on('chat:message', (msg) => {
    if (/mytriggerkey/i.test(msg.content) && msg.inlinerolls) {
      const result = msg.inlinerolls[0].results.total,
        calculatedResult = result*35 + 17;
      sendChat(`player|${msg.playerid}`, `&{template:default} {{name=Roll}} {{Roll was ${result}=Calculated result was ${calculatedResult}}}`);
    }
  });
});


You can pretty that up by ditching the default template and using some HTML API output to drag the result up and left to obscure the double-posting (from player followed by API) and use libInline to restore the roll tooltip.

Or you could wait for it to get fixed......

I've been trying Oosh's suggestion but I am having trouble parsing the JSON structure for inline rolls. 

I see Oosh's "msg.inlinerolls[0].results.total" and I'm able to use that to grab the modified total result. 

However, I'm trying to grab the natural unmodified roll.  I see the JSON structure on the API:Chat page (expanded under "Roll Result Structure Example 1").  And I see that the value that I'm looking for is probably under rolls.rolls.results[0]  or rolls[0].rolls[0] or something like that?  But I'm having trouble lining that up with the msg.inlinerolls[0] part or seeing how I could have found the "results.total" part by myself without  Oosh's example.  I'm just getting "undefined" or "Object object" when I try to experiment by writing parts to the chat. 

So I am hoping Oosh or someone is able to help!

April 21 (3 years ago)

Edited April 21 (3 years ago)
timmaugh
Forum Champion
API Scripter

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
        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); } }