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

Trying to write my first API...

Ok so I am trying to write my own API code.  What I am trying to do is respond to messages like this: /emas "Darryn" is: Unconscious Where "Darryn" is the character name and the condition is after the ":" These are posted from the Beyond20 browser addin when the player sets their condition on their character sheet.  What I am trying to do ultimately is capture when a player sets a condition, then call an API script (CombatMaster 2.0 in this case) to set the condition status for the token.  I wanted to start out by making sure I can capture the chat message, but this is not seeming to work.  I am sure I just missed something easy, but I can't figure it out right now :( on('ready', function() {     on('chat:message', function(msg) {        if (msg.content.indexOf('emas') !== -1) {            sendChat('', 'Here');            //var charname = msg.content.split(' ')[1];            //var c = getObj('character',charname);            //if(c) {            //   sendChat('', "Found: " + charname);            //}        };     }); });
1622906829
timmaugh
Pro
API Scripter
Couple of things... first, I'd suggest sticking to logging for test output -- especially when you're looking to establish what messages this script will pick up versus those it will not. One wrong move on your parameters and the chat message that you send as a confirmation could, itself, trigger your script to send another confirmation, and another... and another... infinite loop style. So instead of sendChat, try  log(...) ... generally. That said, although the API *does* get every message (not just those intended to bypass the chat interface by means of a leading exclamation point), it doesn't always get what you think... because the Roll20 parsers have at the message before the API gets it. In this case, if you drop a: log(msg.content); ...in the chat handler, you'll see that the /emas "Darryn" is eaten, and you only receive the rest of the message. So you'll need another way to detect that you want to do something with this message... and if it is auto-generated from the sheet, you might not have a lot of leeway in what you can change/add. If you can think about a way to control what the command line looks like, this discussion might help with ways to break down your command line once your API receives it.
Thanks.  That has been helpful.  Another question.  I have found that I can use msg.Who to figure out who sent the message (since /emas "Darryn" is stripped off).  And I have found how to find the token based on the "Who"  var c = findObjs({ type: 'character', name: charname})[0]; Is there a way to "select" that token from the API?
1622913263
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Unfortunately, the API cannot act as a user in the GUI. It cannot select objects, only reference or edit them.
1622917281
timmaugh
Pro
API Scripter
Keith is absolutely right that the API can't change the GUI, but MetaScripts can plug some of the holes in that situation. So let's talk about why you want a token selected. If you want the token selected so that you would get the buttons that are set up for token actions, then you're out of luck. The limitation that Keith mentioned is going to stop you. But if you just need the token to be selected for a message, then you're really operating in one of two messages. You're either in the message that began with the EMAS, or in another message that you would like to trigger. Now, MetaScripts only operate on an API message, so those messages that begin with an exclamation point, which means that they wouldn't work on that initial message coming off of the character sheet. But by this point you have determined the token ID already, so you should be able to reference anything on it you need to. You're building that script, so you make it do what you want it to do. If, on the other hand, you have another script or another command line that you want to trigger and you need that particular token to be selected so that when the command line comes up and expects a selected token it would find that one token to be selected, a meta script can help with that. If you can drop a select manager syntax token into your downstream command line, you can virtually select the token. In this case, " virtually " means that the token will only seem to be selected to that message that is generated by the downstream script call. But that might be enough. As an example, if you want to trigger an alterbar call when you see the EMAS message come off of the character sheet, then you could determine and trap that token ID as you are already doing, and then put a select manager syntax token in the alterbar command line; !alterbar ... {& select -M1234567890abcdef} If you can't figure out how to insert that token ID in the select manager syntax structure within the alter-bar command line that might be on a character sheet in an ability, or in a macro, then you could always write it to an attribute on a character sheet and use that in the select manager syntax structure since the roll 20 parser will return the value of that attribute before select manager processes the syntax structure to virtually select the token that matches that ID. If that makes sense.
Yeah, basically I want to capture the /emas command, parse it to know what condition is being set (got that part figured out) and then call a CombatMaster script like this.  But CM requires a selected token !cmaster --add,condition=poisoned,duration=?{Duration|1},direction=?{Direction|-1}
Ok, I have found a workaround for the selection part (though I will look into select manager).  Last question, how do you call an API call from within an API? I tried this sendChat('', '!cmaster --add,condition=unconscious,duration=?{Select token then enter duration|1},direction=?{Direction|-1}') and got this :(
1622926771
The Aaron
Roll20 Production Team
API Scripter
You can't use ?{query} or @{selected} or @{target} references in an api to api call. You need to expand those to a value before sending. 
Can you call a macro from the API? sendChat('', '#Condition-unconscious')
Well I did find a way around it that works, but is nit quiet where I want to get to. on('ready', function() { on('chat:message', function(msg) { if (msg.type !== "api" && msg.content.indexOf(" is: ") !== -1) { var condition = msg.content.split(':')[1]; var charname = msg.who; condition = condition.trim(); var c = findObjs({ type: 'character', name: charname})[0]; if(c) { log("Found " + charname) log('#Condition-unconscious') var msg = '/w ' + charname + '&{template:desc} {{desc=**' + charname + ' is ' + condition + '** \r\n\r\nYou have become [' + condition + '](!
#Condition-' + condition + '). \r\n\r\nPlease select your token and click the link to assign youself the condition\r\n}}' sendChat('GM', msg) } }; }); }); So it builds a template, whispers to that player, and then provides a link to click to run the appropriate macro (obviously it has to exist).  It works, but boy I would like this to automatically do this instead of requiring another click... I'm a .NET windows developer, so I know enough javascript to successfully crash the sandbox at least 3 times everytime I make a change but hey, I have a partial solution for what I want to do :) I'm still open to ideas on how to just have this macro or api call just run, but at least I have a starting point.
1622945852
The Aaron
Roll20 Production Team
API Scripter
If you look at my OnMyTurn script, I've got code in there that modifies an ability to run from the api. You could adapt that to macros. 
1622951348

Edited 1622997848
timmaugh
Pro
API Scripter
If the only reason for the button is that you need the token selected, then SelectManager  can help you out. It looks like you probably have a few macros (differentiated by condition). If you are going to pull the macro text and edit it before sending, anyway, I doubt whether you need so many. You could have 1 that had a replaceable hook for the condition to apply, and a replaceable hook for the token to virtually select. But once you've gone as far as getting the macros reduced down to a single command line (over which you handle replacements), you might as well just put the command line in your script. const getCommand = (condition, token) => {   return `!cmaster --add,condition=${condition}{& select ${token}}`; }; Then you can call it and feed the condition (which you're already determining), and the character name (which you're also determining). SelectManager will look for a token named for that character. So long as there is a token on the page named for that character, it should be found and fed to the CombatMaster message *before* CombatMaster gets it. If you're not convinced that the token will be named for the character, then you could get all the tokens and filter them for where their pageid is the current page and their "represents" property is the character you are dealing with. Either way, you can supply that to the function to build the CombatMaster call. That doesn't account for the direction or duration arguments you were asking for, so if you really need them and you don't have them, you're back to a button. However, if you have them, you can modify the function to include them in the command line. Either way, since you should be able to tell which token you want to affect (and therefore virtually select), you don't have to put it on your players to select their token and worry about them mis-typing the duration. Send the button to you, pre-populated with the {& select... } statement, and you take care it.
Hmm, I am going to have to look into Select Manager, but I wanted to share what I have so far (still using the button method).  It will handle all of these messages now: Darryn has no active conditions Outputs this Darryn is: Blinded, Charmed, Deafened Outputs this on('ready', function() { on('chat:message', function(msg) { if (msg.type === "emote") { log('--------------------------------------------------------') log(msg.type) log(msg.content); var post = '' var index = 0 var charname = msg.who; if (msg.content.indexOf("is: ") !== -1) { var conditions = msg.content.split(':')[1]; conditions = conditions.trim(); log('All ConditionS:' + conditions) var condition = conditions.split(',') log('Split Condition:' + condition) log(condition.length) log(charname + ' ' + msg.content); log("Get the Character"); var c = findObjs({ type: 'character', name: charname})[0]; log(condition[0]); if(c) { log("Found " + charname) post = '&{template:desc} {{desc=**' + charname + ' is ' + conditions + '**' for(index = 0; index < condition.length; ++index) ( post = post + '\r\n\r\nYou have become [' + condition[index].trim() + '](!
#Condition-' + condition[index].trim() + ').' ) post = post + '\r\n\r\nPlease select your token and click the link to assign youself the condition. If there is more than one, only apply the new ones to your character.\r\n}}' } } else if (msg.content.indexOf == ("has no active conditions") !== -1) { post = '&{template:desc} {{desc=**' + charname + ' ' + msg.content + '** \r\n\r\nYou are free from conditions. [Clear Conditions](!
#Clear-Conditions) \r\n\r\nPlease select your token and click the link to clear all your condition\r\n}}' } if (post.length> 0) sendChat('GM', post) }; }); }); So it is a start.  Thanks for the help (I am sure I will need more later)
1623208856

Edited 1623208883
Oosh
Sheet Author
API Scripter
Just a suggestion as I wander past drinking my morning coffee... Combat Master looks like it has a public interface return section at the end. You should be able to call these functions directly without having to go via the Roll20 chat parser. return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers, ObserveTokenChange: observeTokenChange, addConditionToToken, removeConditionFromToken, addTargetsToCondition, getConditions, getConditionByKey, sendConditionToChat, getDefaultIcon }; I don't have any experience with it, but if you figure out the parameters the script is expecting, you should be able to call directly like: CombatMaster.addConditionToToken(tokenObj,key,duration,direction,message);
You know, I never even thought about doing that.  Hmmm.   Even more options to explore.  Thanks.  That's actually a great idea.  I did browse the code briefly, but perhaps I need to read the code in a real editor instead of the little window Roll20 gives us (mind you the logging window is AMAZING for debugging the code).  Thanks @Oosh
Ok I am looking into how to call CombatMaster.addConditionToToken from my script.  Is there something I have to do to make this available or maybe I am misunderstanding the direct call. var c = findObjs({ type: 'character', name: charname})[0]; if(c) {     log("Found " + charname)     for(index = 0; index < condition.length; ++index)     (         log(condition[index].trim().toLowerCase()) CombatMaster.addConditionToToken(c,condition[index].trim().toLowerCase(),1,0,'');     ) } But I get this error unless I comment out the call to CombatMaster Oosh said: Just a suggestion as I wander past drinking my morning coffee... Combat Master looks like it has a public interface return section at the end. You should be able to call these functions directly without having to go via the Roll20 chat parser. return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers, ObserveTokenChange: observeTokenChange, addConditionToToken, removeConditionFromToken, addTargetsToCondition, getConditions, getConditionByKey, sendConditionToChat, getDefaultIcon }; I don't have any experience with it, but if you figure out the parameters the script is expecting, you should be able to call directly like: CombatMaster.addConditionToToken(tokenObj,key,duration,direction,message);
1623526830
timmaugh
Pro
API Scripter
Don't think it's the CM call (at least not yet). An error there would throw a line number at you for where your call failed. This kind of error means that your code did not compile, and I think I can see why. Your for loop needs braces for the loop. Parentheses for the parameters, braces around the commands that run in the loop. for( ... ) {   dothis(...); }
OMG, I can't believe I missed that lol.  It's what you get when you keep looking at something too long I guess.
1623622673
timmaugh
Pro
API Scripter
We've all done it! BTW, you might investigate the .forEach for this, if only for the concision of the lines: let c = findObjs({ type: 'character', name: charname})[0]; if(c) { log(`Found ${charname}`); condition.forEach(cond => { log(cond.trim().toLowerCase()); CombatMaster.addConditionToToken(c,cond.trim().toLowerCase(),1,0,''); } }
1624301527

Edited 1624301620
Ok, so I have been working on this script and used it in a game this past weekend and I was pretty happy with it.  I have added a check upon running to make sure the condition macros are created, and if not create them with description text for the condition.  I would enjoy some critique.  Oh and one thing, as a Windows programmer (from C on up) I HATE javascript curly braces as I can't ever line them up in my head (30 years of looking at braces not on the same line) so I am NOT using that style LOL.  Oh and there are probably too many log() calls right now, but hey, nothing like old school print statements to debug. Oh there is one question also.  Creating a macro requires a player id, so I assumed that the first player found when the script is installed (when it should create the macros) will be the GM.  Is that a safe assumtion? var globalMacros = [ { "action": "You are Blinded.\rA blinded creature can't see and automatically fails any ability check that requires sight.\rAttack rolls against the creature have advantage, and the creature's attack rolls have disadvantage.", "istokenaction": false, "name": "Condition-Blinded", "visibleto": "all", "_playerid":"" }, { "action": "You are Charmed.\rA charmed creature can't attack the charmer or target the charmer with harmful abilities or magical effects.\rThe charmer has advantage on any ability check to interact socially with the creature.", "istokenaction": false, "name": "Condition-Charmed", "visibleto": "all", "_playerid":"" }, { "action": "You are Deafened.\rA deafened creature can't hear and automatically fails any ability check that requires hearing.", "istokenaction": false, "name": "Condition-Deafened", "visibleto": "all", "_playerid":"" }, { "action": "You are Frightened.\rA frightened creature has disadvantage on ability checks and attack rolls while the source of its fear is within line of sight.\rThe creature can't willingly move closer to the source of its fear.", "istokenaction": false, "name": "Condition-Frightened", "visibleto": "all", "_playerid":"" }, { "action": "You are Grappled.\rA grappled creature's speed becomes 0, and it can't benefit from any bonus to its speed.\rThe condition ends if the grappler is incapacitated (see the condition).\rThe condition also ends if an effect removes the grappled creature from the reach of the grappler or grappling effect, such as when a creature is hurled away by the thunderwave spell.", "istokenaction": false, "name": "Condition-Grappled", "visibleto": "all", "_playerid":"" }, { "action": "You are Incapacitated.\rAn incapacitated creature can't take actions or reactions", "istokenaction": false, "name": "Condition-Incapacitated", "visibleto": "all", "_playerid":"" }, { "action": "You are Invisible.\rAn invisible creature is impossible to see without the aid of magic or a special sense. For the purpose of hiding, the creature is heavily obscured. The creature's location can be detected by any noise it makes or any tracks it leaves.\rAttack rolls against the creature have disadvantage, and the creature's attack rolls have advantage.", "istokenaction": false, "name": "Condition-Invisible", "visibleto": "all", "_playerid":"" }, { "action": "You are Paralyzed.\rA paralyzed creature is incapacitated (see the condition) and can't move or speak.\rThe creature automatically fails Strength and Dexterity saving throws.\rAttack rolls against the creature have advantage.\rAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.", "istokenaction": false, "name": "Condition-Paralyzed", "visibleto": "all", "_playerid":"" }, { "action": "You are Petrified.\rA petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.\rThe creature is incapacitated (see the condition), can't move or speak, and is unaware of its surroundings.\rAttack rolls against the creature have advantage.\rThe creature automatically fails Strength and Dexterity saving throws.\rThe creature has resistance to all damage.\rThe creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.", "istokenaction": false, "name": "Condition-Petrified", "visibleto": "all", "_playerid":"" }, { "action": "You are Poisoned.\rA poisoned creature has disadvantage on attack rolls and ability checks.", "istokenaction": false, "name": "Condition-Poisoned", "visibleto": "all", "_playerid":"" }, { "action": "You are Prone.\rA prone creature's only movement option is to crawl, unless it stands up and thereby ends the condition.\rThe creature has disadvantage on attack rolls.\rAn attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the attack roll has disadvantage.", "istokenaction": false, "name": "Condition-Prone", "visibleto": "all", "_playerid":"" }, { "action": "You are Restrained.\rA restrained creature's speed becomes 0, and it can't benefit from any bonus to its speed.\rAttack rolls against the creature have advantage, and the creature's attack rolls have disadvantage.\rThe creature has disadvantage on Dexterity saving throws.", "istokenaction": false, "name": "Condition-Restrained", "visibleto": "all", "_playerid":"" }, { "action": "You are Stunned.\rA stunned creature is incapacitated (see the condition), can't move, and can speak only falteringly.\rThe creature automatically fails Strength and Dexterity saving throws.\rAttack rolls against the creature have advantage.", "istokenaction": false, "name": "Condition-Stunned", "visibleto": "all", "_playerid":"" }, { "action": "You are Exhausted.\rSome special abilities and environmental hazards, such as starvation and the long-term effects of freezing or scorching temperatures, can lead to a special condition called exhaustion. Exhaustion is measured in six levels. An effect can give a creature one or more levels of exhaustion, as specified in the effect's description.", "istokenaction": false, "name": "Condition-Exhausted", "visibleto": "all", "_playerid":"" }, { "action": "You are Unconscious.\rAn unconscious creature is incapacitated, can't move or speak, and is unaware of its surroundings.\rThe creature drops whatever it's holding and falls prone.\rThe creature automatically fails Strength and Dexterity saving throws.\rAttack rolls against the creature have advantage.\rAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.", "istokenaction": false, "name": "Condition-Unconscious", "visibleto": "all", "_playerid":"" } ] function checkInstall() { log("Condition Response Check Install") //log("GM Id: " + gmId) var macros = findObjs({ type: 'macro'}); log("Macros Found: " + macros.length); if (macros.length > 0) log(macros[0].playerid); var conditionMacros = macros.filter((obj) => {return obj.get("name").indexOf("Condition") === 0}) log("conditionMacros Found: " + conditionMacros.length); if (conditionMacros.length !== globalMacros.length) { //var macroName = macro.get('name'); //var macroId = macro.get('_id'); //_.each(conditionMacros, function(macro) {macro.remove()}); createConditionMacros(); } } function createConditionMacros() { var macro log("Creating Condition Macros") log(`Macro: ${macro}`) let allPlayers = findObjs({_type: 'player'}); let player = allPlayers[0]; let playerId = player.get('_id'); log("Player: " + playerId) let isGM = playerIsGM(playerId) log("IsGM: ", isGM) log(player.get('_d20userid')) log(player.get('_displayname')) for (var index = 0; index < globalMacros.length; ++index) { log(`Find Macro: ${globalMacros[index].name}`) let macro = findObjs ( { _type: 'macro', //_playerid: playerId, name: `${globalMacros[index].name}` } )[0]; log(`Macro: ${macro}`) if(!macro) { globalMacros[index]._playerid = playerId; log("Create Macro: " + globalMacros[index].name) createObj('macro', globalMacros[index]); } else { log("Not Creating " + globalMacros[index].name) } } } // Handle Condition on('ready', function() { checkInstall(); on('chat:message', function(msg) { if (msg.type === "emote") { log('--------------------------------------------------------') log(msg.type) log(msg.content); var post = '' var index = 0 var charname = msg.who; if (msg.content.indexOf("is: ") !== -1) { var conditions = msg.content.split(':')[1]; conditions = conditions.trim(); log('All Conditions:' + conditions) var condition = conditions.split(',') log('Split Condition:' + condition) log(condition.length) log(charname + ' ' + msg.content); log("Get the Character"); var c = findObjs({ type: 'character', name: charname})[0]; log(condition[0]); if(c) { log("Found " + charname) post = '&{template:desc} {{desc=**' + charname + ' is ' + conditions + '**' for(index = 0; index < condition.length; ++index) ( post = post + '\r\n\r\nYou have become [' + condition[index].trim() + '](!
#Condition-' + condition[index].trim() + ').' ) post = post + '\r\n\r\nPlease select your token and click the link to assign yourself the condition. If there is more than one, only apply the new ones to your character.\r\n}}' } } else if (msg.content.indexOf == ("has no active conditions") !== -1) { post = '&{template:desc} {{desc=**' + charname + ' ' + msg.content + '** \r\n\r\nYou are free from conditions. \r\n\r\nPlease select your token and remove all your condition icons\r\n}}' } log(post) if (post.length> 0) sendChat('GM', post) }; }); });