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

Can you pass both a selected and target|tokenid to an api script?

1595691892
David M.
Pro
API Scripter
Working on a script I'd like to have require a token to be selected when called, but also allow for passing a target tokenid as an optional argument. In practice, this is seeming like an either/or to me. Sorry if this has been answered on the forums before. My Google skills are failing me. Testing on the fully stripped down basic snippet below. Selecting a token, then calling !test with and without a @{Target|Target Token|token_id}. When I add the target, the msg object no longer acknowledges the selected token. Is this a limitation of the api, or is there some trickery that can be done? I can work around it if I need to, but it's just annoying.  on("ready",function() { on("chat:message",function(msg){ if(msg.type=="api" && msg.content.indexOf("!test")==0 && playerIsGM(msg.playerid)) { log(msg); var selected = msg.selected; if (selected===undefined) { sendChat("API","Error: Please select a character."); return; } } }); });   
1595694848
The Aaron
Roll20 Production Team
API Scripter
You cannot.  There is a long standing bug where using @{target|...} will prevent the API from getting passed the msg.selected array.  You can pass @{selected|...} on the command line, but that will only work for a single selected token, and it will be indeterminate which token it will apply to if you have multiple selected.  If you want to be able to use msg.selected and @{target|...} both, the best option is a two-step API where the first command sends the selected, which is stored, and issues an API command button to the caller that then is used for all the @{target|...} calls you need.  I can write you an example in a little bit.
1595694912

Edited 1595694954
GiGs
Pro
Sheet Author
API Scripter
There is a bug thats been around for a few years when using target with API scripts. You lose the msg.selected data. This isn't an innate limitation of the API, because it used to work properly. An easy way around this to to include the selected id somewhere in the activation as an input parameter. Like !test @{selected|character_id} @{target|character_id} With this approach you do get the character id of the selected character, and can do whatever you need to. Edit:  Ninja'd!
1595713033

Edited 1595714788
David M.
Pro
API Scripter
Thanks, guys! Aaron, that sounds interesting, though also sounds complicated in my case since the @{target|...} is an optional argument among others. Seems like I would have to test for @{target|...}, then if true strip everything else out of msg.content to properly build the API button? This is in reference to the Spawn default token script we discussed a couple days ago. I currently only require one argument: the name of the "character" to spawn. I also currently require a selected token to be the default origin for the spawned token (and some other stuff), but want to allow for optional user input to drop it on/near a target token.  Something like: !Spawn {{ --spawnTok|GenericSpellAoE //required, name of character sheet whose default token will spawn --spawnOriginID|@{Target|Target Token|token_id} //optional, reference point for spawn origin. default is selected token --offset|0,1 //optional, offset (in squares) from the reference origin --sheet|Bilbo Baggins //optional, char sheet to look for ability. default is selected token's sheet --ability|SpellTemplate //optional, ability to call after spawn }} The --sheet and --ability args are there if user wants to automatically call an ability after the spawn occurs (e.g. keithcurtis' SpellFX chat menu ability, existing attack/damage templates, etc.). Note that "--sheet" also defaults to the character sheet of the selected token if omitted. There is another optional argument not shown above (--side|#) to set the currentSide of the token automatically as an alternative to a chat menu ability. Everything pretty much works so far except for this target business. I dunno, maybe it would be easiest to use Gigs' suggestion with @{selected|character_id}, and just make that a 2nd required argument if @target is used. Then, just have @target override @selected for the spawn origin if both are included in the args list, but keep @selected around for the other defaults. While requiring more user diligence to set up (and more error handling), at least it wouldn't require any more clicks to execute. Sigh, seems like this may need to have a --help soon. Or maybe I'm just making things too complicated. [Checks bastardized spaghetti code:] No, I'm *definitely* am making things too complicated ;)
1595723028
The Aaron
Roll20 Production Team
API Scripter
Here's an example of how you could do this: on('ready',()=>{ //////////////////////////////////////////////////////////// // Simple registry functions for storing an object and // retrieving it by ID let store; let retrieve; // destructing assignment of two functions [store,retrieve] = (() => { // closure containing the id counter and storage for msgs const mementos = {}; let num = 0; return [ /* store */ (msg) => { mementos[++num] = msg; return num; }, /* retrieve */ (mid) => { let m = mementos[mid]; delete mementos[mid]; return m; } ]; })(); //////////////////////////////////////////////////////////// // making an array of numbers from 1..n const range = (n)=>[...Array(n+1).keys()].slice(1); on('chat:message',msg=>{ if('api'===msg.type && /^!test(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){ let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); // parse arguments into a hierarchy of objects let args = msg.content.split(/\s+--/).map(arg=>{ let cmds = arg.split(/\s+/); return { cmd: cmds.shift().toLowerCase(), params: cmds }; }); // see if there was a --target argument let targ = args.find(c=>'target' === c.cmd); if(targ){ // REQUEST TARGETS CASE // if --target was specified, find the number of targets to get and whisper a button to the caller let mid = store(msg); let num = parseInt(targ.params[0])||1; sendChat('',`/w "${who}" [Select Targets](!test --memento ${mid} --targets ${range(num).map(n=>`@{target|Pick ${n}|token_id}`).join(' ')})`); } else { // see if this is a button call back for getting targets let marg = args.find(c=>'memento' === c.cmd); if(marg){ // TARGETS SENT CASE let oldMsg = retrieve(parseInt(marg.params[0])); // found the old message if(oldMsg){ let tsarg = args.find(c=>'targets' === c.cmd); sendChat('',`Selected IDs: ${(oldMsg.selected||[]).map(o=>o._id).join(', ')}<br>Targeted IDs: ${tsarg.params.join(', ')}`); } else { sendChat('',`/w "${who}" Targets already selected.`); } } else { // NO TARGETS NEEDED CASE sendChat('',`Selected IDs: ${(msg.selected||[]).map(o=>o._id).join(', ')}`); } } } }); }); Call with one of these sorts of commands: !test --foo bar baz --qux !test --foo bar baz --qux --target 3 The first case will just output the selected token ids. The second case will save the message object, then whisper a button to the caller and ask them to target 3 other tokens.
1595729167
David M.
Pro
API Scripter
Very interesting! It'll take a hot minute for me to digest this. I got it working using the @{selected|...} approach this evening, but I'll see if I can integrate this method to a parallel script and see if it makes it any smoother from a user perspective. Would be a decent re-write considering how I'm currently handling...everything, but I'm intrigued. Now if only I could cancel my plans for tomorrow so I could actually work on it, haha! I'm sure my wife will understand :) I'll check back in in a day or two and let you know how it goes!
1595729448
The Aaron
Roll20 Production Team
API Scripter
=D  It's always nice to have options.  This might not be the ideal solution, but it's a nice tool to understand in case there is a place for it.
1596067245
David M.
Pro
API Scripter
Ok, so finally starting to look into this chat button target approach, and hitting a snag right away. When trying to reference the "range" constant that was defined on("ready"...), I'm getting a "range is not defined" error (greatly cropped code below). Is this because of the order in which range is declared since it is a const, and maybe related to hoisting? I'm trying out event handlers on("ready"...) and my handleInput function is "above" the range declaration. I tried moving the on("ready"...) above handleInput, and also tried having the range const declared in the scope of handleInput, without success. Maybe I just made stupid typos or something, but do you have a recommendation for how to modify in this case?  const TempScriptName = (() => { const scriptName = "TempScriptName"; const version = '0.0.0'; const checkInstall = function() { log('TempScriptName v' + version + ' initialized.'); }; const handleInput = function(msg) { try { if(msg.type=="api" && msg.content.indexOf("!TestSpawn")==0) { Trying to reference "range" somewhere in here! } catch(err) { sendChat('SpawnAPI',`/w "${who}" `+ 'Unhandled exception: ' + err.message); } }; const registerEventHandlers = function() { on('chat:message', handleInput); }; on("ready",() => { checkInstall(); registerEventHandlers(); // Simple registry functions for storing an object and // retrieving it by ID let store; let retrieve; // destructing assignment of two functions [store,retrieve] = (() => { // closure containing the id counter and storage for msgs const mementos = {}; let num = 0; return [ /* store */ (msg) => { mementos[++num] = msg; return num; }, /* retrieve */ (mid) => { let m = mementos[mid]; delete mementos[mid]; return m; } ]; })(); // making an array of numbers from 1..n //----used later in the case of targetID request by user input -->Will create a chat button that stores the original msg.content & selected tokenID const range = (n)=>[...Array(n+1).keys()].slice(1); }); })();
1596074180
The Aaron
Roll20 Production Team
API Scripter
You have it defined in the scope of the function passed to on("ready",...). Move it up under version. 
1596074217
The Aaron
Roll20 Production Team
API Scripter
In fact, move the simple registry stuff up there too. 
1596074391
The Aaron
Roll20 Production Team
API Scripter
There are two ways of writing scripts that I use: 1) all inside a single on ready call. I use this for small snippets.  2) the revealing module pattern, with an object created via a closure, and a small on ready call inside that closure. I use this for large scripts, particularly with a public interface.  You have copied the code out of 1), and placed it in the on ready of 2), but it should be at the closure scope. 
1596107689
David M.
Pro
API Scripter
Ok, I swear that was the first thing I tried, lol! Working now. I was tired from little sleep the night before, so it's possible I did that while changing something else that broke it, or had a copy/paste error. Had a feeling it was something stupid. Thanks, Aaron. Of course now I feel like I just cashed in a good will chip on a silly question. I remember reading someone on another thread saying it was like wasting a Wish... 
1596120964
The Aaron
Roll20 Production Team
API Scripter
Hahaha!  Luckily I have a nearly infinite supply of good will. =D We've all made those sorts of mistakes, and this discussion will help others understand the API abs Javascript a little better in general, so definitely not a waste.