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

[Script] Roll Handout Tables

November 02 (4 years ago)

Edited December 24 (4 years ago)
Jordan C.
Pro
API Scripter
Pending: 

12/24: The one-click version of the script seems to have issues that I am unable to identified; however, the "import" version of the script from the library functions as expected.

Update 2.0 -

Now works with DMG tables! (Huge thanks to Aaron for helping me test the new tables)

The script has been completely rewritten and functions with nearly all tables I could think to test excluding a few 'problem' tables that I will list below. Table naming has been improved though a select few can still return lengthy names. 

Should be ready for One-Click on 12/22

Updates:

  • Table names replace spaces with '_' and removes commas, colons, and parens.
  • Can handle multi-column tables such as Treasure Hoard tables.
  • Can create multiple tables for split dice tables (e.g. Passages table that uses d12 or d20).
  • Works with multiple dice roll tables, but does not create accurate item weights for them (e.g. 3d8)
  • Amended: Now handles specified instances of multiple dice rolls (3d4, 2d6, 3d6, and 3d8)
  • Added a very crude toggle to stop writing handout links when you enter !handoutlinks off, and can turn it back on using !handoutlinks on. This setting does not save once the game is reset. I haven't figured out how to do that part yet.
  • Will now create items/tables when it sees something like "10 or lower" or "3d8+CHA", but still can't account for previous influencing rolls/stats

Tables with known issues:

  • Childhood memories: (3d8 + CHA), Running a business(d100+days), Carousing (d100+level): Creates tables but can't account for adjusted weights from previous modifiers.
  • Selling a Magic Item (d100 + mod.)
  • Siblings and Home table from Tasha's Origins (requires subtracting numbers which item weight cannot account for
  • Magic Items Table I, Villains Methods and Schemes, has 'subtables' within the table(I've marked this as too niche to fix for the time being but subject to change)
  • Designing NPC tables that have nested tables, (e.g. Ideals)

Until the update is accepted, you can find the full script here: Github Repo

1.5: Added better table naming handling, specifically for the tables in Tasha's Cauldron of Everything. Now works with every table I could think to test from the compendium. Names can get lengthy, but it's difficult to catch all conditions otherwise. Not yet updated with one-click but available via github link. Set default rolls to whispers.

1.4: Cleaned up the formatting and used better variable naming, replaced crude handler with a cleaner one for endless loop that occurs (setting notes triggering change handout event). Thanks Aaron for the code framework/suggestion for that

1.3: Added better variable handling for links and added " " in front of items that start with digits that aren't rolls. Fixed a bad variable name.

Hello all,

Now available via one-click install

I finished the first version of a script that will search all handouts in your game, check if they contain a rollable table, create that table if they do, then write a link above that table in the handout notes that will roll from that table into chat. Many thanks to The Aaron and Keith for the help/idea for this script.

Notes:

  • This is a great way to organize and keep track of mass amouts of roll tables since you can use folders with handouts!
  • Parses all handouts using "!rollhandout" as an api chat command
  • This script was written to work in conjunction with recursive tables but there are standard 'roll' links included along side the 'roll recursive' links. One caveat I haven't addressed is that it will always surround any dice roll with brackets (e.g. 1d6+2 turns into [[1d6+2]])
  • The script as is will run at the start of a game being launched and scan every handout immediately. If you wish to prevent this, there are lines that can be uncommented. (1, 9, 14, 218, and 222 I believe; they all have comments labelling this feature).
  • The script will parse any handouts that are added. For example, dragging "Arctic Encounters" from the 5e compendium to the game will create a handout, proc the script to make tables for each tier and add a link to that handout automatically.
  • I am unaware of any conflicts at the moment, but if you notice any variable names that might be an obvious conflict I am happy to adjust this.
  • I am very, very new to this so if anything at all stands out as a potential problem or just bad practice I won't be at all offended by it being pointed out.
  • I know some formatting for the chat commands could be improved, but I plan to handle that after some feedback for what people may be looking for, if anything.

I hope some may find this script useful and if you encounter any errors please let me know! My tests were run mostly with the standard tables from 5e compendium so it is possible different tables may cause an issue.


If you have any suggestions or recommendations for improving this I am more than happy to learn how to implement them if possible.


Thanks!

November 02 (4 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Hi Jordan,

I have tried importing a couple of handouts with tables but am unable to get the script to create buttons. I am using the Arctic Encounters test you mention. Also, the sandbox crashes unless I comment out the first and last lines. Any idea what I am doing wrong?

November 02 (4 years ago)
Jordan C.
Pro
API Scripter


keithcurtis said:

Hi Jordan,

I have tried importing a couple of handouts with tables but am unable to get the script to create buttons. I am using the Arctic Encounters test you mention. Also, the sandbox crashes unless I comment out the first and last lines. Any idea what I am doing wrong?

Keith, 

Do you know what the API sandbox throws as the error? It can technically work without the 'on ready' it will just re-analyze everything at each start of the game.

As for running the script to accommodate handouts that are added in after the initial pass, the lines that need to be reinstated should look like the ones below (it's possible I got the line numbers wrong):

Line 1: on('ready', function() { //comment out for first run

Line 9: on('change:handout', function(obj) { //comment out for first run

Line 14: i++; //comment out for first run

Line 218: )}; //end change:handout - comment out for first run

Line 222: )}; //end on ready - comment out for first run


I made a fresh game and ran the script by itself and it functioned as expected so there may be a conflict with another script? Another possible troubleshoot is to delete the handout from the journal and drag it over again. 



November 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

I have only skimmed the script, and am too tired to read it properly, but I'm puzzled by the commented out sections.

The first and last lines, commenting out the on(ready) statement are most baffling to me. What purpose and effect is this meant to have? All on('ready'_ does is delay the start of the script until the campaign is fully loaded, and as I understand it, there is no benefit to having those lines commented out, and uncommented them wont change the function of the script at all - but will make it more robust.


Another question: if you were to uncomment them all, you'd have an on('change:handout') event nested inside an on('add:handout') event. That doesnt seem right to me. 


Final question: you have this loop:

for (r; r < row.length; r++) {

but you have earlier (line 55 i think) defined

let r = 0

The for loop will be using that r value, and will not start at zero. Is that intended?

I'm guessing it doesnt set it at let r= 0, it is intended, it just struck me as odd and worth asking about.

 

Regarding your uncommenting approach: it would be better to handle things like this without uncommenting. Instead set a variable near the start of the script, which can maybe have a value of 0 or 1, and when coming to those commented lines, check that variables value, and only do the operations if the value is 1.

That makes it much easier for people to switch functions and stuff off and on - you just tell them to change the value at the start of the script.

November 02 (4 years ago)

Edited November 02 (4 years ago)
Jordan C.
Pro
API Scripter
The first and last lines, commenting out the on(ready) statement are most baffling to me. What purpose and effect is this meant to have? All on('ready'_ does is delay the start of the script until the campaign is fully loaded, and as I understand it, there is no benefit to having those lines commented out, and uncommented them wont change the function of the script at all - but will make it more robust.

The intent I had was to analyze all handouts that already exist in a game and are being adding as the game loads. For instance if someone has a few dozen handouts with rollable tables already made they would have to delete them then re-add them for the script to run and parse them. Instead, you can comment out those lines on its first run and have all handouts be parsed and reinstate them afterwards


Another question: if you were to uncomment them all, you'd have an on('change:handout') event nested inside an on('add:handout') event. That doesnt seem right to me. 

The intent here is to not run the script every time a handout is personally edited. If it only used the change:handout event it would run anytime someone added something trivial which I figured could just be bypassed by using add:handout. If I used add:handout by itself, it would read the notes section as null for any handout that is dragged over since it seems to create the handout, then change it as a separate event. If I am missing something here, let me know.


Final question: you have this loop:

for (r; r < row.length; r++) {

but you have earlier (line 55 i think) defined

let r = 0

The for loop will be using that r value, and will not start at zero. Is that intended?

I'm guessing it doesnt set it at let r= 0, it is intended, it just struck me as odd and worth asking about.

I may not have gone about it in the most elegant way by I was intending to reset and define r as 0 and only have it start at 1 if a certain condition is met. It's entirely possible that it is completely unnecessary and already has a value of 0 without that line. The intent here was to address the lack of uniformity in the tables that get created from compendium items. Below are two examples from the Sorcerer handout that is created by dragging it into the game; in the first example the relevant table information starts at a different row than the second example. I have found at least 3 separate examples of tables: Has a <thead> tag but is blank, has <thead> that isn't blank, doesn't have <thead> section. The simplest method I thought of was just setting the starting row at the relevant number in regards to the loop. 


Example 1:



Example 2:



Regarding your uncommenting approach: it would be better to handle things like this without uncommenting. Instead set a variable near the start of the script, which can maybe have a value of 0 or 1, and when coming to those commented lines, check that variables value, and only do the operations if the value is 1.

That makes it much easier for people to switch functions and stuff off and on - you just tell them to change the value at the start of the script.

That's a great solution, thank you! Definitely my laziness showing there but that's an easy enough way to handle those.


Thanks for the feedback!

November 02 (4 years ago)

Edited November 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

I haven't had the chance to read this script yet, sorry about that. =(

I will say that if you want to get this included in the Roll20 Repo, you will want all configuration to not require editing the script.  I suggest the state object for that.  Regarding the events, it would likely be better to wait until on('ready',...) has fired, then just do a findObjs({type:'handout'}) and handle each of them there (likely with a deferred queue).  The problem with registering event handlers inside of events is that you end up with as many handler calls as the number of times your event occurs.  Consider if you have twenty handouts in your game, registering an on('change:handout',...) event in your on('add:handout',...) handler means the next time you change a handout, you're handler for editing it will be called 20 times to do the same work.

November 02 (4 years ago)
Jordan C.
Pro
API Scripter


The Aaron said:

I haven't had the chance to read this script yet, sorry about that. =(

I will say that if you want to get this included in the Roll20 Repo, you will want all configuration to not require editing the script.  I suggest the state object for that.  Regarding the events, it would likely be better to wait until on('ready',...) has fired, then just do a findObjs({type:'handout'}) and handle each of them there (likely with a deferred queue).  The problem with registering event handlers inside of events is that you end up with as many handler calls as the number of times your event occurs.  Consider if you have twenty handouts in your game, registering an on('change:handout',...) event in your on('add:handout',...) handler means the next time you change a handout, you're handler for editing it will be called 20 times to do the same work.


No worries, and thanks for the feedback!

I'll have to admit that I don't fully grasp what is going on with regards to why the event handler goes 20 times for having 20 handouts. In its normal state I have it set to start with on('ready. For my purposes of editing existing tables just the one time I found it easier to just bypass a few lines briefly then return it to the state it would function in for the rest of its intended use. I think I was understanding the add/change events as "if a handout is added, and it is followed by that handout being changed, then proceed with the script." In my testing that has been my experience, but is that not what is happening?

Would it be better to just have the on('change: and have it activate the few times someone edits a handout without a table?

As for the deferred queue and findObjs, I haven't learned quite enough yet to understand that fully; I would be concerned that it tries to parse hundreds of handouts just to find the one that has a new table, which likely shows that I would be implementing it incorrectly so any help there would be wonderful.


For simplicity sake I can easily remove the section about commenting out lines and the script functions as intended, it just won't retroactively add tables to handouts. From there I can update a newer version once I have worked out how to implement the states.

November 02 (4 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

It was error on my end. I didn't fully understand which lines to comment out for which purpose. I will try again this evening.

November 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Event handlers are not specific to a single instance of a Roll20 type.  If you register an event handler for 'change:handout', it will get called for every handout that is changed, regardless of where you created the handler.  The event system behind the on() function has a list of functions to call when a matching event occurs.  Each time you call it, it will add the supplied function to the list for that event.  It does not remove duplicates, so if you call it with the same function 100 times, add 100 copies of that function to its list, and it will call that same function 100 times when that 1 event occurs.  If you are adding event handlers inside the handler for another event, then they will get re-added each time that event occurs, leading to duplicates.

Would it be better to just have the on('change: and have it activate the few times someone edits a handout without a table?

The way you have it now will not prevent it from being called for every handout that is edited, table or no.  You will need to inspect the contents of the handout to determine if you want to parse it regardless.  You could require a naming convention in the handout title to make it easy to pare down the list, but you'll still need to parse the contents and determine if they are valid.

As for the deferred queue and findObjs, I haven't learned quite enough yet to understand that fully; I would be concerned that it tries to parse hundreds of handouts just to find the one that has a new table, which likely shows that I would be implementing it incorrectly so any help there would be wonderful.

As it stands, you're already parsing all those handouts at start up, it's just happening as many single calls to your event handler as the API spins up. (which you are ignoring with commented/uncommented commands, but should really manage differently)  A deferred queue looks something like this:

let handouts = findObjs({type:'handout'});
const parseOneHandout = () => {
  if(handouts.length){
    let handout = handouts.shift();
    // do something with handout;

    // defer next handout
    setTimeout(parseOneHandout,0); 
  }
};
// start the deferred processing
parseOneHandout();

 

Adding a chat command to kick of the parsing of all handouts, or possibly a filtered subset would be a way to dynamically kick off parsing of a subset of handouts, or parsing at a time of choice.


November 02 (4 years ago)

Edited November 02 (4 years ago)
Jordan C.
Pro
API Scripter

Okay, it will take me some time to digest everything but thanks for your patience and describing it thoroughly!

As for the commenting/uncommenting part I totally concede that it needs to be removed and I am now looking at the script without that as part of the equation. I will come at that after overcoming the event problems it has.


If you register an event handler for 'change:handout', it will get called for every handout that is changed, regardless of where you created the handler.

I think my confusion is stemming from not seeing the effects of this in my logs during testing. For instance, I had logs directly after each event saying things like "handout xyz created" and "handout xyz changed"; with what your describing, would the script not create logs for every handout that exists? And wouldn't it send that log when a handout is edited even without the 'add:handout' event? Or let's say there were a bunch of logs within the function after that part like "log("no table found"), wouldn't it log 100 times if the function was stored 100 times?

Quick edit: I think I am also confused since the logs/script seemed to work exactly as expected and took next to zero time to complete its tasks, which may not be a relevant factor to the topic but is contributing to my brain block.

I apologize for not fully understanding and I certainly don't want any of this to come across as stubborn, so please forgive me if it is. 

As it stands, you're already parsing all those handouts at start up, it's just happening as many single calls to your event handler as the API spins up. (which you are ignoring with commented/uncommented commands, but should really manage differently)  A deferred queue looks something like this:

Thank you for the example, that definitely makes more sense now!

November 02 (4 years ago)

Edited November 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

No worries.  I'll try to work up a little example script that illustrates it more eloquently. =D

November 02 (4 years ago)
Jordan C.
Pro
API Scripter

Thank you!

In the meantime I have added your deferred queue suggestion and now it no longer has any elements of nested events or anything like that. It simply activates on "!rollhandout" chat command.

Now it doesn't handle when a handout is created by dragging, but would it cause any issues to simply name the function that is called for the on('chat:message') event and also call it on an 'change:handout' event? Or can I handle the single object from that change handout event without having to deal with the other handouts? Or am I just running in a mental circle and would have the same problem I had before 

November 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

I'd do something like:

on('ready',()=>{

  const processSingleHandout = (handout) => {
    // do stuff here with handout
  };

  const processAllHandouts = () => {
    let handouts = findObjs({type:'handout'});
    const parseOneHandout = () => {
      if(handouts.length){
        let handout = handouts.shift();
        processSingleHandout(handout);

        // defer next handout
        setTimeout(parseOneHandout,0); 
      }
    };
    // start the deferred processing
    parseOneHandout();
  };


  const onChangeHandout = (obj, prev) => {
    processSingleHandout(obj);
  }

  on('chat:message',(msg)=>{
    // some chat stuff that calls processAllHandouts()

  });

  on('change:handout',onChangeHandout);

});


November 03 (4 years ago)

Edited November 03 (4 years ago)
Jordan C.
Pro
API Scripter

Thank you tons for the help! 

I am gonna spend some time to figure out exactly what I just did but your framework worked well; I included a similar section to the last version that basically stops the process when it 'sets' notes since it technically would proc the change handout event again. (I did so using i as a random variable and a crude if function: (i > 0 ? i = 0 and return) essentially).

Slightly confused by the (obj, prev) and how it works in place of 'handout' but I am sure I can read up on that.

I also want to adjust the chat command that is sent by the standard roll but I am having difficulty with something like "1 adult red dragon" turning into just 1 in the chat. I am sure I will be able to find some information on that as well.


Again, thank you. For the framework but also the patience! 

November 03 (4 years ago)

Edited November 03 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

The names of the arguments for event functions are just convention. The first one, commonly obj, is the Roll20 object that caused the event in its current (final) state, so after the changes have been applied. The second, commonly prev, is a plain Javascript object with the properties the Roll20 object had before the event. For example, if someone moved a token from left 70 to left 140, the "change:graphic" event would fire and obj would be that graphic object, with obj.get("left") returning 140, and prev would be a Javascript object with a property left of 70 (and all the other properties the same as what you'd .get() them on obj. 

API changes shouldn't trigger events, with rare exception.

Rollable tables entries that start with a number are taken as die rolls. To prevent that, you need something in front of the number. A space might work, or &nbsp; or the like. 

November 03 (4 years ago)
Jordan C.
Pro
API Scripter


The Aaron said:

The names of the arguments for event functions are just convention. The first one, commonly obj, is the Roll20 object that caused the event in its current (final) state, so after the changes have been applied. The second, commonly prev, is a plain Javascript object with the properties the Roll20 object had before the event. For example, if someone moved a token from left 70 to left 140, the "change:graphic" event would fire and obj would be that graphic object, with obj.get("left") returning 140, and prev would be a Javascript object with a property left of 70 (and all the other properties the same as what you'd .get() them on obj. 

API changes shouldn't trigger events, with rare exception.

Rollable tables entries that start with a number are taken as die rolls. To prevent that, you need something in front of the number. A space might work, or &nbsp; or the like. 


Noted! Those both are clear now.

It would return 'maximum call stack size exceeded' without that bit being added so I just assumed that's what was happening.

And okay, that should be easy enough to account for, thanks!

November 03 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Events would be asynchronous, so wouldn't be limited by the callstack size.  I'll have to look at the code, but probably you have accidentally created a runaway recursive function.

November 03 (4 years ago)
Jordan C.
Pro
API Scripter

Ah, interesting I must really be missing something then - I removed the code that catches the error so that it would return the call stack exceeded again, then I only commented one line of code out and there was no more error. Here's the line I omitted:

handout.set('notes', notesNew);

November 03 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Well!!  No, you're totally right, that must be causing a change event!!!   I'd consider that a bug.

It's probably cleaner to have an array of changed handout IDs and manipulate it, but using a counter like you have it works. 

November 03 (4 years ago)
Jordan C.
Pro
API Scripter

Oh as in:

Push handout ID into a temporary cache

Set notes

And have the change event check if the ID exists in the temporary cache before continuing?


If so I think I can add that 

November 03 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Yeah, something akin to;

// just below on('ready',...)
let activeChangeIds = [];

// in the processSingleHandout() just before change
activeChangeIds.push(handout.id);

// in the onChange function
if(activeChangeIds.includes(obj.id)){
  activeChangeIds.remove(obj.id);
} else {
  processSingleHandout(obj);
}



November 04 (4 years ago)
Jordan C.
Pro
API Scripter

Gotcha! The crude handler in there occupied those spaces so it was an easy swap, thanks!

It is now at version 1.4 and is much cleaner than the other versions; if there is anything more that would be good to add/change for it to be an acceptable addition as a proper script I am happy to continue working on it.

November 17 (4 years ago)

Edited November 20 (4 years ago)
Jordan C.
Pro
API Scripter

For anyone looking to import tables from Tasha's Cauldron of Everything, the table formats used upon handout creation are different (yet again somehow) and do not all work with the script. I am looking into it and will hopefully have it addressed quickly without breaking anything else.

Edit: Decided to just add a basic if statement that determines whether the section only has one table and if so it uses that as the table name preceded by the handout name. Will ignore if the table name was already formed from the section header.  Spoke too soon, the RollTable handouts have zero headers which breaks this script right now. Working on it. Maybe.


Edit: Now works with Tasha's tables.

December 06 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Nice!  Have you tried it with the DMG tables?

December 06 (4 years ago)
Jordan C.
Pro
API Scripter

I have been having trouble posting so hopefully this goes through:

I haven't done any testing for the DMG as I haven't purchased it yet. I am not sure I plan to at the moment but I am more than happy to update the script if if issues arise!

December 09 (4 years ago)
Jordan C.
Pro
API Scripter

Now works with DMG tables thanks to Aaron's support! This includes treasure hoard tables, dungeon mapping, wilderness mapping, etc.

Even works on that pesky d12/d20 table too now ;)
December 09 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Nice!  I've got to say that script is a lot neater than I initially realized.  I love the embedded roll links in the handouts, very chic!

December 09 (4 years ago)
Jordan C.
Pro
API Scripter
Well frankly the first version was pretty rough. But the idea for the script and that clever design suggestion were provided by none other than keithcurtis
December 09 (4 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Hi Jordan, It's looking really slick!

I installed in a brand new campaign and got no results until I realized I had not installed Recursive Tables. Is there a way to test for that and give a warning to new users?

December 09 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

This:

    if('undefined' === typeof RecursiveTable){
        setTimeout(()=>sendChat('Roll Handout Tables',`/w gm <div style="background:#ff9999;padding:.5em;border:3px solid darkred;border-radius:1em;line-height:1em;color:darkred;"><b>Roll Handout Tables</b> requires the script RecursiveTable, which can be installed from the 1-Click Script Library.</div>`),1000);
    }

December 09 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Of course, in 1-click, you can just declare RecursiveTable as a dependency and it will install it, but it's nice to check for peeps that copy/paste the source.

December 09 (4 years ago)
Jordan C.
Pro
API Scripter

Keith, that warning should probably exist for everyone with api access in general lol.

And wow that was fast! Just added it in, much appreciated.

December 10 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

(I may have had 95% of that code sitting around for similar purposes... =D)

December 10 (4 years ago)
Jordan C.
Pro
API Scripter

Now enters proper weights for 3d4, 2d6, 3d6, and 3d8 tables!

December 24 (4 years ago)
Jordan C.
Pro
API Scripter

Update:

For some reason, the script doesn't function properly when installed via the one-click library "Add Script" button; however, it does work if you use the "import" option. I am currently unable to identify what is causing the issue. 

The problem I was able to notice was a naming and parsing issue for the Astral Planes handout from the DMG. It seems to be creating only 1 out of 3 tables and naming them all incorrectly. I tried duplicating this with the script manually added to the API sandbox and found that the problem didn't persist there. Maybe there is an issue with the JSON, but I can't identify where it might be.