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

Help with storing data (especially object ids) in gmnotes for API reference

I am coding an api (I guess mod is technically correct) that will use the native door and window system but will update the multisided graphic of the door using token mod. While I can post my spaghetti code in progress, here is the gist of my "design": (1) manually identify the object ids of the doors and dooricon objects (2) put the object id of the corresponding door in the dooricon object's gmnotes, and name the dooricon objects "dooricon" (for ease of finding them) (3) on a door change, it finds all of the dooricon objects and, for each of them, checks their gmnotes. If the gmnotes (decoded from URI component) = the actual door's id, then the graphic will change depending on whether the door is open or closed (via token mod). Problem: for whatever reason, the result of decodeURIComponent from gm notes doesn't function in a way I can use it to (for example) check if the ids match. When I have debugged it using the chat, it appears as though the decoded id from gmnotes is a different font than the id you get from obj.id? I have tried using both code font and regular, to no avail. Any tips on how to make these match? I know people prefer to use state to store data, but if I'm being honest I have found it intimidating, and manually using gmnotes is just easier for my brain and workflow.
1726171073
timmaugh
Pro
API Scripter
Hey, Joshua... The font that it is using in the log output panel won't matter to how you get or use the contents of the gmnotes field. JavaScript has no idea what font you're using when you code; it just cares about what it says when you're finished. The process you are describing should work. Can you share what code you're using, now (again, font doesn't matter)? That might let us better see what might be going wrong.
Here is the relevant part of my api code, per your request: on ('change:door', function(obj) {     var doorobjid = obj.id     //doorobjid is the object id of the door that was changed, so opened or closed     var doors = findObjs({         _pageid: Campaign().get("playerpageid"),         _type: "graphic",         layer: "objects",         name: "dooricon"     });     //finds all tokens on the object layer named dooricon, all of which are multi-sided tokens     _.each(doors, function(obj) {         var gmNotes = obj.get("gmnotes");         var dooriconid = obj.id;         //dooriconid is the object id of the dooricon object         var doorid = decodeURIComponent(gmNotes);         if(doorid === doorobjid) {             var doorobj = getObj('door', doorobjid)             //this is done to test if the actual door is open or closed             sendChat('test', 'door id is same')             //in all of my testing, I have never gotten the above message to trigger, so somehow they are not considered equivalent...even though when I check their values they appear the same             if(doorobj.isOpen === true) {                 sendChat('test', 'door is open')                 sendChat('system', '!token-mod --ids '+dooriconid+' --set currentside|1');             };             if(doorobj.isOpen === false) {                 sendChat('test', 'door is closed')                 sendChat('system', '!token-mod --ids '+dooriconid+' --set currentside|2');             };         }     }); });
1726246181
timmaugh
Pro
API Scripter
OK, I think the issue is that when you decode the gmnotes, you get the HTML formatting, too. So something like: <p>-M1234567890abcdef</p> ...will never match: -M1234567890abcdef There are a few ways to get around that... the simplest would probably be just testing for the presence of those <p> tags. Change this line: if(doorid === doorobjid) { ...to be... if(`<p>${doorid}</p>` === doorobjid) { That should do it. "...but if you leave the camera rolling..." That will work if all you are doing is storing that one piece of data in the gmnotes field. There might come a time where you'll want to use another script that requires other information to be in the gmnotes field, too, so I'd suggest going for a slightly different option that gives you a little more flexibility. Let's imagine that any information could be in the gmnotes, but if the dooricon's ID shows up on a line all by itself, that's what we're looking for. Testing any single line can use a regex like this: /<p>\s*([a-zA-Z0-9-_]+)<\/p>/ That will put the "thing that might be the ID" within the group 1 returns. So you could do a  matchAll  against the regex and then test if any of the group 1 returns match our id. That would look more like this: on('change:door', function (obj) {     let doorobjid = obj.id     //doorobjid is the object id of the door that was changed, so opened or closed     let doors = findObjs({         _pageid: Campaign().get("playerpageid"),         _type: "graphic",         layer: "objects",         name: "dooricon"     });     let idrx = /<p>\s*([a-zA-Z0-9-_]+)\s*<\/p>/gm;          //finds all tokens on the object layer named dooricon, all of which are multi-sided tokens     doors.forEach(d => {         let gmNotes = decondURIComponent(d.get("gmnotes"));         if (gmNotes.matchAll(idrx).some(v => v[1] === doorobjid)) {             let dooriconid = d.id;             var doorobj = getObj('door', doorobjid)             //this is done to test if the actual door is open or closed             sendChat('test', 'door id is same')             //in all of my testing, I have never gotten the above message to trigger, so somehow they are not considered equivalent...even though when I check their values they appear the same             if (doorobj.isOpen) {                 sendChat('test', 'door is open')                 sendChat('system', '!token-mod --ids ' + dooriconid + ' --set currentside|1');             } else {                 sendChat('test', 'door is closed')                 sendChat('system', '!token-mod --ids ' + dooriconid + ' --set currentside|2');             };         }     }); }); Note that I changed the underscore "_.each" function to be the native ".forEach()" function, and also that you'd duplicated/overwritten your "obj" variable within this function. You used the same name for the object being passed into the callback of the "on(change:door)" at the top, do I changed the later iteration through the array of doors to be just "d". (Above code is untested, btw). "...but if you leave the camera rolling..." For a simple change (like the visible side of a token), you can make the change to the dooricon yourself, without having to use TokenMod, if you want. I'll leave it to you to put together, but it would just be using the .set() function on the "d" object. That's discussed at the top of this page .
1726249034
The Aaron
Roll20 Production Team
API Scripter
Just a note, the biggest complication with setting the currentside in the production API is that it won't change the token image, so to make it work, you'll need to decode the sides property, grab the correct image there and use it to set imgsrc.  Be sure to still set current side so that the UI doesn't break.  
1726251209
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I was not aware that referencing the image of the door was possible. This has potential for: Lockpicking scripts You could store values in the token that a script could use to prompt a lockpicking skill or strength check, and then change the status of the door accordingly  Auto open/close scripts Imagine a DL line that appears or disappears over a window depending on the proximity of a PC token. To look out a window you would have to walk over to it. Hmm. That might not even require referencing the image.
1726263076

Edited 1726266902
Thank you for all of the feedback! A couple of issues from that script and the suggestions though. When I took that code and tried to use it (changed spelling of decode to work) it gave me an error saying gmNotes.matchAll(...).some() was not a function. When I tried using your direct correction (the addition of `<p>${doorid}</p>`), it didn't seem to work either (subsequent testing revealed it was still not "equal"). I tried flipping your addition to the other variable, just in case the addition was meant to add to the other variable, not take away from the first, but it still didn't work. Any further advice would be greatly appreciated!  UPDATE: I used the log function to compare them, and this is what gmNotes (after the decoder) returned with: "<p><span style=\"font-size: 13.65px ; font-family: "proxima nova" , , , "blinkmacsystemfont" , "segoe ui" , "roboto" , , "ubuntu" , "cantarell" , "helvetica neue" , sans-serif\">-O6Y_naUQDgY9zSOBeJF</span></p>" What would I need to do to isolate JUST the id portion ("-O6Y_naUQDgY9zSOBeJF")? Thank you in advance for the help!
1726268127
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Use the regex pattern: /-[A- Za -z0-9_]{ 19 }/
1726279544
timmaugh
Pro
API Scripter
ahh... sorry. What's probably happening where you get ".some() is not a function" is that when a matchAll finds no matches testing against the regex, it doesn't give you an array, so the ".some()" function can't be located. You could correct that by changing the IF line to read: if ((gmNotes.matchAll(idrx) || []).some(v => v[1] === doorobjid)) { ...except we'd still have the regex which would be wrong, being built to look for the paragraph html tags. Honestly, if I were to do what you're doing, I'd turn the id you're *looking* for into a regex, and search all of my doors that way. It doesn't force the id to exist on a line all by itself, so there might be an edge case where you have some other verbiage in the notes section of a token and that verbiage contains the id...? ... maybe? If you're not worried about edge case, you could use the door's id as a regex to search all of the door graphics gmnotes like the following. This is about as compact as this gets: on('change:door', function (obj) {     let doors = findObjs({         _pageid: Campaign().get("playerpageid"),         _type: "graphic",         layer: "objects",         name: "dooricon"     });     let idrx = new RegExp(obj.id,'gm');     sendChat('test', `Door is ${obj.get('isOpen') ? 'open' : 'closed'}`);          doors.forEach(d => {         if (idrx.test(decondURIComponent(d.get("gmnotes")))) {             sendChat('test', `door id of ${obj.id} was found on door icon token with id of ${d.id}`);             sendChat('system', `!token-mod --ids ${d.id} --set currentside|${obj.get('isOpen') ? '1' : '2'}`);         }     }); }); A couple of other things I saw when I took a closer look at your code... 1) You don't have to go get the door based on the id you get from the obj that is passed into your event listener... that obj object IS the object, itself. You already have it and can read the isOpen property from the obj 2) that said, I haven't done a ton with doors, but I think that the "isOpen", "isSecret", and "isLocked" properties are all Roll20 special properties that have to be retrieved using the "get" syntax, i.e.: obj.get('isOpen') 3) I removed a lot of the variable assignments... if there wasn't a need to keep the variable for some other line elsewhere in the code (which, for this simple example, there wasn't). Of course, if you need to refer to the variable elsewhere, you can add those back in
1726279939

Edited 1726279955
timmaugh
Pro
API Scripter
BTW, if you *are* worried about that edge case where the id of the door could be in the gmnotes of the dooricon but NOT represent the linkage of this dooricon to that door, then you can get around that by instituting a little more formatting into your gmnotes. You'd just need a text reference to look for in addition to the id. For instance, you would be looking for somewhere in the gmnotes where the id was used following another pattern, like: DoorLink:  -O6Y_naUQDgY9zSOBeJF So, anywhere the ID followed the pattern of "DoorLink: " would indicate that this icon was linked to that door. To accommodate that, you'd have to change the regex declaration line in the code above to be: let idrx = new RegExp(`DoorLink:\\s\+${obj.id}`,'gm');
1726337783
The Aaron
Roll20 Production Team
API Scripter
keithcurtis said: I was not aware that referencing the image of the door was possible. This has potential for: To be clear, you can't access the image for a door or window object.  What the OP is doing is keeping a graphic which is sync'd to the state of the door by the script.  The graphic's name is 'dooricon'.
1726344013
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Ah, I thought they were saying the interface icon was referenceable (at least for the purposes of storing a note). Still Syncing a graphic (or any editable object) to a door or window object would allow the script usages I described.
1726349983

Edited 1726355855
The Aaron said: Just a note, the biggest complication with setting the currentside in the production API is that it won't change the token image, so to make it work, you'll need to decode the sides property, grab the correct image there and use it to set imgsrc.  Be sure to still set current side so that the UI doesn't break.   How would I go about doing that? Within the API documentation I haven't found a specific "sides" property, so I'm not sure how I would go about doing this....any pointers? I have tried to find the token mod base code (to see if I could find clarification there), but have, so far, not been successful. From what I have seen elsewhere, mod api should be capable of using another api (such as token mod) by sendChat(), yet each time I try to do so it doesn't work. What am I missing?
keithcurtis said: Ah, I thought they were saying the interface icon was referenceable (at least for the purposes of storing a note). Still Syncing a graphic (or any editable object) to a door or window object would allow the script usages I described. Based on your suggestion I have been considering expanding my script to include some of your suggestions, in particular the lock and secret system. Once I finish the open and closed functionality, I plan to add in data to prompt a check for lockpicking, and one to check the passive perception of players, such that a "secret door" (normally hidden) would automatically "appear" (secret turned off) if a character token with a high enough passive perception is within "x" range. I know you can do versions of this with "It's a trap," but having it built into the doors makes sense in my head. If you have any other ideas that might increase the utility of my script, let me know!
1726355677

Edited 1726355729
The Aaron
Roll20 Production Team
API Scripter
Joshua G.  said: How would I go about doing that? Within the API documentation I haven't found a specific "sides" property, so I'm not sure how I would go about doing this....any pointers? I have tried to find the token mod base code (to see if I could find clarification there), but have, so far, not been successful. Something like this would probably work: const SetTokenSide = (obj,n) => { let sides = obj.get('sides'); let idx = parseInt(n); if(isNaN(idx)){ log(`SetTokenSide(): n of [${n}] is invalid. Use an integer value for side index.`); return; } if(sides.length){ sides = sides.split(/\|/).map(decodeURIComponent).map(getCleanImgsrc); } else { sides = [getCleanImgsrc(obj.get('imgsrc'))]; } if(idx>=0 && idx<sides.length){ if(undefined === sides[idx]){ log(`SetTokenSide(): n of [${n}] is not in a user library, cannot set via API.`); return; } obj.set({ currentSide: idx, imgsrc: sides[idx] }); } else { log(`SetTokenSide(): n of [${n}] out of range. Use an index from 0 and ${sides.length-1}.`); } }; I've not tested that, but it should get you there.  You can just call: SetTokenSide(SomeToken,3); It will handle bounds checking, and some index related problems, as well as images the API can't use. Note : the index is 0-biased, so the first one has index 0, and the last one out of four would have index 3.
This addition, plus your provided code, have worked perfectly (insofar as my code can now compare the data). The issue I am now running into is that the sendChat() with the command doesn't appear to actually trigger the command in question. I've double checked by sending in chat the command without the "!", and the copying and pasting it myself, and then it works. Does a command send in chat by an api not register as a gm, and is that the reason its not working? timmaugh said: BTW, if you *are* worried about that edge case where the id of the door could be in the gmnotes of the dooricon but NOT represent the linkage of this dooricon to that door, then you can get around that by instituting a little more formatting into your gmnotes. You'd just need a text reference to look for in addition to the id. For instance, you would be looking for somewhere in the gmnotes where the id was used following another pattern, like: DoorLink:  -O6Y_naUQDgY9zSOBeJF So, anywhere the ID followed the pattern of "DoorLink: " would indicate that this icon was linked to that door. To accommodate that, you'd have to change the regex declaration line in the code above to be: let idrx = new RegExp(`DoorLink:\\s\+${obj.id}`,'gm');
1726356941

Edited 1726410285
timmaugh
Pro
API Scripter
A command for a script sent *by* a script has 4 notable differences: 1) There are no selected tokens (the Script Moderator is like a player at your table, sending the message, except that they can't ever have tokens selected) 2) The 'playerID' property is 'API' 3) The 'who' property is also 'api' 4) It can't use queries since, obviously, the Script Moderator would have to be the one to answer the prompt With regard to the first 3, SelectManager (a metascript) helps with those. It comes preconfigured to help wtih the selected tokens, but you can configure it to help with the other 2, as well. As for what's happening with TokenMod... there are a couple of TokenMod things that might come into play. One is a configuration that lets players use ids (I think that one is --players-can-ids). You set it once, and it is in effect. The other (and probably the one you want) is a command line argument (so you want it every time your script would fire a message for TokenMod): --api-as <playerid> That will instruct it to pretend that the playerid you supply sent the message. Give that a try and let us know.
I tried your code and initially got an error saying getCleanImgsr was not defined. When I took a look at the code for token mod on github, I realized that was a function defined within it, so I copied its definition in as well, and while this time there were no errors per say, it didn't seem to work. I am trying to follow up timmaugh's suggestions at the moment while I wait to hear back. For y'all's convinence, here is my current script: &nbsp; //the getClean Imgsrc was taken straight from token mod on github....when I searched the function name, that's what came up &nbsp; const getCleanImgsrc = (imgsrc) =&gt; { &nbsp; &nbsp; &nbsp; let parts = (imgsrc||'').match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); &nbsp; &nbsp; &nbsp; if(parts) { &nbsp; &nbsp; &nbsp; &nbsp; let leader = parts[1].replace(/^https:\/\/files.d20.io\//,'<a href="https://s3.amazonaws.com/files.d20.io/" rel="nofollow">https://s3.amazonaws.com/files.d20.io/</a>'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return `${leader}thumb${parts[3]}${parts[4] ? parts[4] : `?${Math.round(Math.random()*9999999)}`}`; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; const SetTokenSide = (obj,n) =&gt; { &nbsp; &nbsp; let sides = obj.get('sides'); &nbsp; &nbsp; let idx = parseInt(n); &nbsp; &nbsp; if(isNaN(idx)){ &nbsp; &nbsp; &nbsp; log(`SetTokenSide(): n of [${n}] is invalid.&nbsp; Use an integer value for side index.`); &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if(sides.length){ &nbsp; &nbsp; &nbsp; sides = sides.split(/\|/).map(decodeURIComponent).map(getCleanImgsrc); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; sides = [getCleanImgsrc(obj.get('imgsrc'))]; &nbsp; &nbsp; } &nbsp; &nbsp; if(idx&gt;=0 &amp;&amp; idx&lt;sides.length){ &nbsp; &nbsp; &nbsp; if(undefined === sides[idx]){ &nbsp; &nbsp; &nbsp; &nbsp; log(`SetTokenSide(): n of [${n}] is not in a user library, cannot set via API.`); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; obj.set({ &nbsp; &nbsp; &nbsp; &nbsp; currentSide: idx, &nbsp; &nbsp; &nbsp; &nbsp; imgsrc: sides[idx] &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; log(`SetTokenSide(): n of [${n}] out of range. Use an index from 0 and ${sides.length-1}.`); &nbsp; &nbsp; } &nbsp; }; on('change:door', function (obj) { &nbsp; &nbsp; let doors = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; _pageid: Campaign().get("playerpageid"), &nbsp; &nbsp; &nbsp; &nbsp; _type: "graphic", &nbsp; &nbsp; &nbsp; &nbsp; layer: "objects", &nbsp; &nbsp; &nbsp; &nbsp; name: "dooricon" &nbsp; &nbsp; }); &nbsp; &nbsp; let idrx = new RegExp(`DoorLink:\\s\+${obj.id}`,'gm'); &nbsp; &nbsp; sendChat('test', `Door is ${obj.get('isOpen') ? 'open' : 'closed'}`); &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; doors.forEach(d =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (idrx.test(decodeURIComponent(d.get("gmnotes")))) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('test', `door id of ${obj.id} was found on door icon token with id of ${d.id}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //sendChat('', `!token-mod --ids ${d.id} --set currentside|${obj.get('isOpen') ? '1' : '2'}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('isOpen)')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SetTokenSide(d, 0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SetTokenSide(d, 1); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); });
Success! While getting the image natively, via Aaron's code, hasn't yet worked (I assume my definition of getCleanImgsrc is wrong in some way), timmaugh's suggestions has worked! As long as I include the --api-as &lt;my playerid&gt;, the command works perfectly! :D