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] SelectManager - Utility to Preserve the Selected Tokens For API-Generated Calls (coding Best Practice)

1605631674

Edited 1612761014
timmaugh
Pro
API Scripter
SelectManager Current Version (and link): 0.0.4 (Currently in my personal repo, pending a submission to the one-click.) This script is a joint project between myself and TheAaron bringing added functionality to scripts (or maybe returning functionality). Concept Abstract : Preserve the selected ( v0.0.4 EDIT : and who and playerid) property of the message object from any user-generated api call so it is available to any other script called *from* the API. Normally, API-generated script calls do NOT have a selected property (or it is undefined), and the other properties get reset. Example : Bob has a script called Burninate . Burninate does a certain amount of work and then it can, optionally, fire off another script if you supply the command line. Bob would like to fire off the script GoodJorb , but GoodJorb relies on having tokens selected at the time you make the call. By using Burninate to call GoodJorb , the resulting message object that reaches GoodJorb does not contain the selected tokens. By including SelectManager in the installed scripts and by a small change to GoodJorb (which, hopefully, all scripters will take to including as a best practice going forward), the selected tokens that reach Burninate (as a user-generated call) will be available to GoodJorb , even though it will be started from the API. Syntax There is no syntax to implement for the user. You will not call this script directly. Can't get easier than that, can it? Just install it so that your other scripts can be upgraded to use it. Scripters might eventually update their own scripts, but you can update them yourself if you want. Read on to find out how... Implementing for Other Scripts (API-Proofing) -- Suggested Best Practice Going forward, as a "coding best practice", we'd suggest scripters implement one of 2 paths for utilizing SelectManager. This will "api-proof" your script against the possibility that another script might call yours. Option 1: Require SelectManager as a script dependency As of v0.0.4, SelectManager is on its way to the one-click. Once it arrives, you will be able to designate your script as having a dependency on SelectManager. Once that is the case, you can access the library directly: msg.who = SelectManager.GetWho(); msg.playerid = SelectManager.GetPlayerID(); msg.selected = SelectManager.GetSelected(); Option 2: Make it Optional This example show how to implement the GetSelected() function off of the SelectManager library. You can extrapolate from these a similar process for implementing any of the other functions that you might need from SelectManager. FIRST , add the following 2 lines to the outer scope of each script to api-proof: let getSelected = () => []; on('ready', () => { if(undefined !== typeof SelectManager) getSelected = () => SelectManager.GetSelected(); }); SECOND , add this line in whatever code handles the on('chat:message') event, directly after the test of whether the script should answer the api call. Be sure to replace 'msg' with whatever name the script is using for the message object received by the handler (examples will follow, below): if('API' === msg .playerid) msg .selected = getSelected(); Note for Scripters ("why" a best practice) SelectManager is fairly simple to reverse engineer and see how it works: it listens to all api calls, detects those that are user-generated, and tracks the selected property of the message object in the state variable. Any and/or all scripts could do this independently, but to avoid needlessly bloating the state variable and to minimize the overhead of many scripts taking the same action, we suggest we all standardize around this implementation. Then, as the interface is enhanced with future development, you will be able to take advantage of it.  Then we just encourage users to install SelectManager with any game, no matter the other scripts that will be installed, just as a way to extend the base Roll20 functionality. Change Log: Version 0.0.3 - Initial Release Version 0.0.4 (link) - Added GetWho() and GetPlayerID()
1605632561

Edited 1605632922
timmaugh
Pro
API Scripter
Upgrading Fat-Arrow Scripts Some scripts will be declared like this: const scriptname = (() => { //... stuff happens here })(); Here is an example of a basic form, including the necessary lines: const receiver = (() => {     // INCLUDE THESE 2 LINES IN THE OUTER SCOPE     let getSelected = () => [];     on('ready', () => { if(undefined !== typeof SelectManager) getSelected = () => SelectManager.GetSelected(); });     handleInput = (msg) => {         // <-- using 'msg' as the name of the message object         // next line tests the message's api handle to see if this script should respond         if(!('api' === msg.type && msg.content.match(/^!receiver\b/)) ) return;         // INCLUDE THIS LINE AFTER THE API CHECK         if('API' === msg.playerid) msg.selected = getSelected();         sendChat('RECEIVER',`PLAYERID: ${msg.playerid}<br>${JSON.stringify(msg.selected)}`);             };          const registerEventHandlers = () => {         on('chat:message', handleInput);     }          on('ready', () => {         registerEventHandlers();         // alternatively, scripters could put the on('ready') check in an existing on('ready') block (replaces the same line, above)         // if(undefined !== typeof SelectManager) getSelected = () => SelectManager.GetSelected();     });     return {              };      })();
1605636025
GiGs
Pro
Sheet Author
API Scripter
What a great concept. This looks very handy.
1605636376

Edited 1612464699
GiGs
Pro
Sheet Author
API Scripter
Part of the set up there isnt explained very well. If I've understood correctly, I would suggest changing that example script template to something like  this. const receiver = (() => {     // INCLUDE THESE 2 LINES IN THE OUTER SCOPE     let getSelected = () => [];     on('ready', () => { if(undefined !== typeof SelectManager) getSelected = () => SelectManager.GetSelected(); });      // if the script already has an on('ready') block, you can remove the line above. See on('ready') below.     handleInput = (msg) => {         // <-- using 'msg' as the name of the message object         // next line tests the message's api handle to see if this script should respond         if(!('api' === msg.type && msg.content.match(/^!receiver\b/)) ) return;         // INCLUDE THIS LINE AFTER THE API CHECK         if('API' === msg.playerid) msg.selected = getSelected();         sendChat('RECEIVER',`PLAYERID: ${msg.playerid}<br>${JSON.stringify(msg.selected)}`);             };          const registerEventHandlers = () => {         on('chat:message', handleInput);     }          on('ready', () => {         registerEventHandlers();         // remove the following line if using the one line on(ready) function from way up above.         if(undefined !== typeof SelectManager) getSelected = () => SelectManager.GetSelected();              });     return {              };      })();
1605640114
timmaugh
Pro
API Scripter
Thanks, GiGs! Yes, that rendering of the example is accurate to what the user could do. I like having both my rendering and yours up as comparisons; it might take people seeing it 2 different ways to really "get it." Besides, I'm not sure how much people will be comfortable rooting around in a script to find an existing on('ready') block, and it doesn't hurt to have the extra block by pasting that self-contained line. I think that would be more for scripters who know exactly where their on('ready') block is.
1605643088

Edited 1605643129
David M.
Pro
API Scripter
Super nifty, guys! Would best practice also be to include SelectManager as a dependency for the parent script in the one-click? If so, not sure how this works exactly. Is it just a matter of adding "SelectManager" to the script.json under the "dependencies" key (assuming SelectManager eventually gets on the one-click, that is)? Another clarification: Does just the api-triggered script need the SelectManager references, or does the original script need it, as well. For example, I am updating one of my scripts to allow it to be called from powercards (by adding a token_id argument to my script). Can I just instead add the SelectManager references to my script (and add as dependency) and call it a day, or would powercards also need to be updated? Looking over the code with my feeble brain, it looks like only my script would need it, since SelectManager is just constantly overwriting the latest selected tokens in its state object? Just double checking :)
1605643470
The Aaron
Roll20 Production Team
API Scripter
Only the client of SelectManager would need the references to it. Also, you're right about the dependency stuff.  There are a few scripts that make use of it that you could look at for an example, It's a Trap is one I believe.
1605643830
timmaugh
Pro
API Scripter
The way it is suggested to be implemented isn't a true dependency, per se. If the user has SelectManager installed, it will be detected and the interface assigned to the getSelected() function. If not, then you're basically in the same boat as you are now (no msg.selected for an api-generated call). That was the least-intrusive way of introducing the idea. Scripters are free to create the dependency, of course, especially if the script is absolutely dependent on the selected tokens to run properly. As for which script needs this, it would be the downstream script... any script that could be called by another script. For that script, you want to make sure you can access the selected array that was created with the very first call in the chain. IOW, yes: if powercards will be calling your script, you have to update yours. =D I will see if I can get the JSON and MD created to get it submitted to the Roll20 repo in time for this week. Cheers!
1605643988
timmaugh
Pro
API Scripter
Aaron's forum-fu is strong.
1605644693
David M.
Pro
API Scripter
Thanks for the clarifications (and the gif lolz)!
1605703348
David M.
Pro
API Scripter
Another quick check: when manually adding a dependent script (i.e. SelectManager), I seem to recall that it needs to be installed before the parent script, due to concatenation of the scripts prior to compiling or something. So, will I need to uninstall my primary script, install SelectManager, then re-install the primary script? 
1605706861
timmaugh
Pro
API Scripter
I've tested with SelectManager both before and after client scripts, and the order didn't matter. It might just be for the particular way SelectManager is implemented, leaning on the way the api handles calls to other scripts asynchronously, that makes it not matter. SelectManager picks up the message object from the first call (user-generated) before the call to the call to the next script (api-generated) resolves a new message object. Now, as for whether something specific about the dependency mechanic in the 1-click requires that the other script be there before you install the dependent script... that I couldn't tell you.
1605708009
Pat
Pro
API Scripter
Hoisting. Declaration order doesn't matter for global-level functions, and SelectManager is not a dependency so much as an add-on by the look of this. It tries to find it, which by hoisting it would do if they're all in one big blob, and if it's not there it moves on. 
1605708462
David M.
Pro
API Scripter