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

Callback function question for _defaultToken

1595533900
David M.
Pro
API Scripter
js noob here, having a little trouble understanding of how the callback function works for getting the JSON string of the default token for a given character sheet. I eventually want to be able to create an instance of the default token (which in this case will be a rollable table token) on the map. I imagine this last part should be pretty straightforward, assuming I can get the correct object information in the first place.   In the code below, I've defined a variable defaultTokenString that I want to eventually contain the parsed JSON string. Since the _defaulttoken property must use a callback function rather than a simple Obj.get("defaultToken"), I am currently assigning the string value in the bolded code block below. I would expect my two log values below to contain the same information. However, only the log within the callback outputs the parsed JSON representing the default token object. My defaultTokenString remains undefined. This remains true if I don't declare it in advance, but wait until the line in which it is assigned. on("ready",function() { on("chat:message",function(msg){ if(msg.type=="api" && msg.content.indexOf("!Test")==0 && playerIsGM(msg.playerid)) { var defaultTokenString = "" let args = msg.content.split(/\s+--/); args.shift(); if (args.length >= 1) { let inputName = args[0]; let check = findObjs({ _type: "character", name: inputName },{caseInsensitive: true})[0]; if (typeof check == 'undefined') { sendChat(msg.who, "Character named \"" + inputName + "\" not found."); } else { let chars = findObjs({ _type: "character", name: inputName },{caseInsensitive: true});      defaultTokenString = chars[0].get("_defaulttoken", function(defaultToken) {          JSON.parse(defaultToken);          log('defaultToken = ' + defaultToken);         }); } log('defaultTokenString = ' + defaultTokenString); } } }); }); I have also tried assigning the string from within the callback: chars[0].get("_defaulttoken", function(defaultToken) {     JSON.parse(defaultToken);     log('defaultToken = ' + defaultToken);     defaultTokenString = defaultToken }); When I do this, defaultTokenString is no longer undefined, but it instead remains an empty string as if the assignment never took place. What is happening here? 
1595535504

Edited 1595537678
timmaugh
Forum Champion
API Scripter
I'll run a test to be sure... but I think I see 2 issues. The defaultToken variable is the thing you are passing into the callback... that means that when you're doing the JSON.parse, you have to assign the result to something else... chars[0].get("_defaulttoken", (defaultToken) => {     defaultTokenString = JSON.parse(defaultToken);     log(`defaultTokenString = ${defaultTokenString}`); }); But this *also* (issue #2) looks like it's asynchronous... so bear that in mind that defaultTokenString would only contain value when the asynchronous call completes... which means you've basically forked your code at this point... the rest of your code completes, *then* the asynch stuff happens. If I'm right, you want to have your implementation of defaultTokenString downstream of this callback (ie, within the callback, or initiated by the callback).
1595536917
timmaugh
Forum Champion
API Scripter
The documentation doesn't say that the retrieval of defaultToken is async, but I'm guessing it is because of it being a blob format. You can test by writing your defaultTokenString in two places. chars[0].get("_defaulttoken", (defaultToken) => {     defaultTokenString = JSON.parse(defaultToken);     log(`IN THE CALLBACK: defaultTokenString = ${defaultTokenString}`); }); log(`OUTSIDE THE CALLBACK: defaultTokenString = ${defaultTokenString}`); My guess is that the one that says "Outside the Callback" will be empty... because at that point in the code, the callback hasn't resolved. Async code's interaction with normal code looks like this: synchronous code start         ||     do synchronous stuff         ||     do synchronous stuff         ||     call asynchronous stuff ========> |         ||                          ||     do synchronous stuff             ||         ||                        ascynch waiting     do synchronous stuff             ||         ||                           ||     finish synchronous stuff         ||         | <===========================|     asynch running         ||     finish asynch stuff Asynch will wait for a window to run, so short of using promises or semaphore, your best bet is just to break your code into a a new downstream, synchronous chain at this point. Alternately, Aaron gave me a tip that you can collect things like this during an "on('character:change') event. I think there's a specific event for changing each property of a character, so on('character:change:defaultToken'). Collect the info and put it somewhere you can access it later (state variable namespace). That part will run asynchronously and you'll never know it. Then, when you need it, you'll be writing synchronous code to retrieve it from the state. Post back if you need an example of putting this into the state under a namespace.
1595541189
The Aaron
Roll20 Production Team
API Scripter
Yeah, it's asynchronous, meaning the value becomes available.&nbsp; Here's a quick bit that explains Asynchronous Functions:&nbsp; <a href="https://wiki.roll20.net/API:Use_Guide#A_Treatise_on_Asynchronous_Functions" rel="nofollow">https://wiki.roll20.net/API:Use_Guide#A_Treatise_on_Asynchronous_Functions</a> Basically, you have to "pay it forward" with your code when you're dealing with asynchronous functions.&nbsp; The easiest way to do that is just passing the rest of your code as the body of the callback function.
1595542066
David M.
Pro
API Scripter
Sorry, it is definitely asynchronous, as the two logs in my original code would come back out of order. That was probably important information :) This is implied in the documentation because it states that _defaulttoken is similar to notes, gmnotes, and bio properties, which themselves get explained&nbsp; here . What surprised me was that defaultTokenString was showing up as "undefined" in the code that I posted. I would have expected either an empty string or the "correct" assigned value, depending on whether the asyc "get" had completed or not. Maybe at the point in time when I tried to log it, it is in some weird state where it was neither the original value nor the eventual one, so it just returns "undefined". Running your async test returned this in the log: "OUTSIDE THE CALLBACK: defaultTokenString = " "IN THE CALLBACK: defaultTokenString = [object Object]" Neither one returns the parsed JSON.&nbsp;&nbsp; Btw, I'm not trying to change any character properties. Basically just trying to create something like King's !Summon script that will work to summon a rollable table token that I will have set up previously. The "character" I'm summoning is actually going to be a Spell Origin [rollable table] token that I will be setting the side to later in the script. Basically an automated implementation of keithcurtis' SpellFX trick illustrated&nbsp; here . The !Summon script does not handle rollable table tokens, as it just sets the&nbsp;imgsrc. I could add sides with TokenMod, but would have input each url. Since the "sides" property is part of the Character.defaultToken JSON, I was thinking I could parse it out and set the sides when I create the token. That way I can freely add/delete/modify images to the rollable table without any extra hassle. Asynch will wait for a window to run, so short of using promises or semaphore, your best bet is just to break your code into a a new downstream, synchronous chain at this point. I've read a little bit about promises, but the implementation is confusing to me. Not sure what you mean by breaking into a new downstream synchronous chain. Is there a simple example you could give (not necessarily using my specific problem/syntax)? Maybe like asynchonously getting the number of legs a dog has and using it later, or something?
1595549686
The Aaron
Roll20 Production Team
API Scripter
Variables that are not initialized have the special value undefined.&nbsp; undefined is like null or NaN, a predefined object.&nbsp; You can compare to it to see if something is undefined: let foo; if( undefined === foo ){ log('foo has not been defined yet and has the special value "undefined"); } When you log the variable, the log function converts it to the string "undefined", but it's value isn't the string "undefined", it's the value undefined. Here are two examples, the first is traditional procedural code, the second will be similar async code with a chain of callbacks: let output = "Something: "; let bio = character.get('bio'); // won't work, async let gmnotes = character.get('gmnotes'); // won't work async sendChat('', `${output}${bio}${gmnotes}`); let output = "Something: "; character.get('bio',(bio)=&gt;{ // callback 1 &nbsp;&nbsp;&nbsp;&nbsp;character.get('gmnotes',(gmnotes)=&gt;{ // callback 2 &nbsp;&nbsp;&nbsp;&nbsp; sendChat('', `${output}${bio}${gmnotes}`); &nbsp;&nbsp;&nbsp;&nbsp;}); });
1595556435
David M.
Pro
API Scripter
Thanks, Aaron and timmaugh for your responses! I thought I had initialized to an empty string with: var defaultTokenString = "" ...in my code above. Does the scope of that variable not carry into subordinate nested code blocks (the if....then block when I logged it)?&nbsp; I'll play around with the callbacks some more tomorrow when I'm not half asleep. Thanks again! &nbsp;
1595556818
The Aaron
Roll20 Production Team
API Scripter
Won't matter.&nbsp; Here's the actually execution order of your script: on("ready",function() { on("chat:message",function(msg){ if(msg.type=="api" &amp;&amp; msg.content.indexOf("!Test")==0 &amp;&amp; playerIsGM(msg.playerid)) { var defaultTokenString = "" let args = msg.content.split(/\s+--/); args.shift(); if (args.length &gt;= 1) { let inputName = args[0]; let check = findObjs({ _type: "character", name: inputName },{caseInsensitive: true})[0]; if (typeof check == 'undefined') { sendChat(msg.who, "Character named \"" + inputName + "\" not found."); } else { let chars = findObjs({ _type: "character", name: inputName },{caseInsensitive: true}); &nbsp;&nbsp;&nbsp;&nbsp; defaultTokenString = chars[0].get("_defaulttoken", function(defaultToken) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); } log('defaultTokenString = ' + defaultTokenString); } } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// some time laster &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;JSON.parse(defaultToken); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log('defaultToken = ' + defaultToken); }); }); Note that .get() is not going to return a value anyway.
1595562406

Edited 1595562433
timmaugh
Forum Champion
API Scripter
First, JSON parsing does not return a string or value necessarily. It returns the kind of object that was encoded into the JSON. I haven't retrieved the defaultToken like this, but seeing your [object Object] output, I think what you have at the end of getting the defaultToken is a token (graphic) object. You can test by asking for properties or calling methods that you would expect from a token from the object (call it something other than defaultTokenString because ... well... it's not a string). chars[0].get("_defaulttoken", (t) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;defaultToken = JSON.parse(t); &nbsp;&nbsp;&nbsp;&nbsp;log(`IN THE CALLBACK: defaultToken.id = ${defaultToken.id}`); }); If all else fails, try to log JSON.stringify of your parsed object, and see what it tells you. log(`IN THE CALLBACK: stringify = ${JSON.stringify(defaultToken)}`); Now, on to the async stuff. Let's say that you have a normal synchronous process to (...checks what you had asked for in the example...) count the legs on a dog. That process might look like: let dog = findObjs({ type: "doggo", who:"the best boy in the whole wide world" })[0]; let legs = findObjs({ type: "legs", id: dog.id }); let legcount = legs.length; Straightforward. In this case, the legs are stored as separate objects from the dog (that's... an odd and disturbing way to say that), but tied by the ID. The last line is seems superfluous, but I included it for comparison. Now imagine the same process, but this time the legs are stored as a property (object) on the dog object, and it has to be accessed asynchronously and utilize a callback. If you don't handle it right, your synchronous code is going to finish first, and it won't work... let dog = findObjs({ type: "doggo", who:"the best boy in the whole wide world" })[0]; let legs; dog.get("legs", l =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;legs = l; }); if (!legs.hasOwnProperty("leftfront")) return; log(`There are ${Object.entries(legs).reduce( (a, l) =&gt; { return a++;}, 0 ); You won't ever see anything hit the log because legs will never have a "leftfront" property at this point. And, yes, counting the legs this way is superfluous, but it shows a way that the code could expand, needing to perform some operation on the legs. Aaron's suggestion and my suggestion for dealing with that are basically the same. Aaron said to put everything inside the callback. That would look like this: let dog = findObjs({ type: "doggo", who:"the best boy in the whole wide world" })[0]; let legs; dog.get("legs", l =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;legs = l; &nbsp;&nbsp;&nbsp;&nbsp;if (!legs.hasOwnProperty("leftfront")) return; &nbsp;&nbsp;&nbsp;&nbsp;log(`There are ${Object.entries(legs).reduce( (a, l) =&gt; { return a++;}, 0 ); }); Now legs will get properly filled and the count will show the accurate number... which might be 4, or 3, or... or 8. My suggestion is the same idea, but just break the subsequent code (what we just moved into the callback) into its own procedure. The effect is no different: you are still letting the synchronous code end, which opens the door to the asynchronous callback, and then you proceed from there. Your asynchronous "later" becomes your new "now". const getDog = () =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;let dog = findObjs({ type: "doggo", who:"the best boy in the whole wide world" })[0]; &nbsp;&nbsp;&nbsp;&nbsp;let legs;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;dog.get("legs", l =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;legs = l; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!legs.hasOwnProperty("leftfront")) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;processLegs(legs); &nbsp;&nbsp;&nbsp;&nbsp;}); }; const processLegs = (legs) =&gt; { log(`There are ${Object.entries(legs).reduce( (a, l) =&gt; { return a++;}, 0 ); }; Doing it this way, you're writing processLegs() as if it is synchronous... which... once your "later" becomes your "now", it is. The same way it would be if it were still in the callback. However you get a couple of benefits... first, you don't have to deal with the nested structures and indented white space in your code, making for cleaner code. Secondly, and more importantly, you could divide the logic and reuse the nested function. For instance, if you had multiple ways to arrive at having a leg object but you wanted to process it the same way every time, then you might want to have that portion of the logic separated, anyway. Maybe the user designated a new leg they needed to add to their dog. That information would be received synchronously, and you would be calling the processLegs() function synchronously. It's a simple example, and probably overkill/superfluous for the task, but hopefully this helps illustrate the idea.
1595596721
David M.
Pro
API Scripter
Wow, thank you guys so much for the in-depth explanations and "filling in the gaps" of the API Objects wiki! It was very educational. Going off of this, I got things to work this morning by simply calling a worker function from within the callback for the default token. The following spawns the default token (including all sides of a rollable table token) of a given "character" in the journal, adjacent to a selected token. e.g. !spawnDefault --GenericAoESpell on("ready",function() { function spawnTokenAtXY(tokenJSON, pageID, spawnX, spawnY) { let baseObj = JSON.parse(tokenJSON); baseObj.pageid = pageID; baseObj.left = spawnX; baseObj.top = spawnY; createObj('graphic',baseObj); } on("chat:message",function(msg){ if(msg.type=="api" &amp;&amp; msg.content.indexOf("!Test")==0 &amp;&amp; playerIsGM(msg.playerid)) { var selected = msg.selected; if (selected===undefined) { sendChat("API","Please select a character."); return; } //set spawn point from selected token properties let tok = getObj("graphic",selected[0]._id); let spawnPageID = tok.get("pageid") let spawnLeft = tok.get("left") + 70&nbsp;&nbsp;&nbsp;&nbsp;//spawn to adjacent right of selected token (currently hardcoded, expand later with args?) let spawnTop = tok.get("top") let args = msg.content.split(/\s+--/); args.shift(); if (args.length &gt;= 1) { var inputName = args[0]; var check = findObjs({ _type: "character", name: inputName },{caseInsensitive: true})[0]; if (typeof check == 'undefined') { sendChat(msg.who, "Character named \"" + inputName + "\" not found."); } else { var chars = findObjs({ _type: "character", name: inputName },{caseInsensitive: true}); chars[0].get("_defaulttoken", function(defaultToken) { spawnTokenAtXY(defaultToken, spawnPageID, spawnLeft, spawnTop); }); } } } }); }); Should be trivial to proceed from here. Now if only I didn't have to go into work, I could finish this dang thing! :) I really appreciate your time, you guys are awesome!
1595597308
timmaugh
Forum Champion
API Scripter
Cheers! Keep it going! And, for the record, Aaron is awesome. I'm just... some. But I'm working toward 'awe' everyday. ;-)
1595604966
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I'm really looking forward to seeing this finished script. Updating my spell token is a bit of a hassle. I also use a "scene" token for theater of the mind that might also benefit from this.
1595617668
David M.
Pro
API Scripter
Haha, timmaugh! Keith, I was originally thinking to keep it simple (the above headache with defaulttoken notwithstanding). Just spawning the spell origin rollable token and bringing up the chat menu for spell type with one click. Also passing the side/size of the token as an optional argument. Was just trying to avoid having to go into the journal, dragging the token out, selecting the character, then calling the chat menu with token action and going back to the chat tab. So, the process of editing the sides of the spell token and updating the Ability chat menu would remain unchanged with this initial approach. I hadn't really thought about the setup process. I suppose creating/editing the chat menu Ability through the script based on the default token would be possible, though there would potentially be a lot of input (Spell token name/ID, destination char name/ID, list of button text associated to sides, height/width, and z_order). Would this be any easier than just editing the token action directly? Since I've just been theorycrafting to date, what is it about the setup that is the biggest hassle?
1595619983
The Aaron
Roll20 Production Team
API Scripter
Just a quick note to avert some heartache: the API can only create graphics which are in a User Library. The createObj() function will fail if passed a Marketplace image.&nbsp;
1595623134
David M.
Pro
API Scripter
Yes, I had seen that on the forums when researching this. The rollable table token would have to be set up with that (super annoying) limitation in mind. I had planned on making my own images and uploading to the library. Thanks!