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)

November 17 (4 years ago)

Edited February 08 (4 years ago)
timmaugh
Forum Champion
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()
November 17 (4 years ago)

Edited November 17 (4 years ago)
timmaugh
Forum Champion
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 {
        
    };
    
})();


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

What a great concept. This looks very handy.

November 17 (4 years ago)

Edited February 04 (4 years ago)
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 {
        
    };
    
})();


November 17 (4 years ago)
timmaugh
Forum Champion
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.

November 17 (4 years ago)

Edited November 17 (4 years ago)
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 :)

November 17 (4 years ago)
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.

November 17 (4 years ago)
timmaugh
Forum Champion
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!

November 17 (4 years ago)
timmaugh
Forum Champion
API Scripter

Aaron's forum-fu is strong.


November 17 (4 years ago)
David M.
Pro
API Scripter

Thanks for the clarifications (and the gif lolz)!

November 18 (4 years ago)
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? 

November 18 (4 years ago)
timmaugh
Forum Champion
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.

November 18 (4 years ago)
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. 

November 18 (4 years ago)
David M.
Pro
API Scripter