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

Is the information in the Bio and Info Tab callable by macro?

1524523291
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I cannot find this information in the wiki. I currently use the GM notes section (of the token) to send GM info to the chat, but I thought it would be nice to include an API button in that output to give me the option of sending public info to the players.
1524523581
The Aaron
Pro
API Scripter
Nope.  But if you need an API to do it, I know someone that could work that out for you... =D
1524526539
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Before I ask, would it be a minor tweak from the GMnotes script? Maybe I can do it... gulp.
1524533965
The Aaron
Pro
API Scripter
Very minor. =D
1524535149
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I know I have to find the represents property of the token to get the character, and that that character has a property called "bio". I can get that much from the API:Objects page of the wiki. Here's the existing script: on('ready',function(){           'use strict';                  const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=>String.fromCharCode(parseInt(m.slice(2),16)));                         on('chat:message',function(msg){               if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){                   let match=msg.content.match(/^!gmnote-(.*)$/),                       regex;                   if(match && match[1]){                       regex = new RegExp(`^${match[1]}`,'i');                   }                                                          _.chain(msg.selected)                       .map( s => getObj('graphic',s._id))                       .reject(_.isUndefined)                       .reject((o)=>o.get('gmnotes').length===0)                       .each( o => {                           if(regex){                               let lines=_.filter(decodeURIComponent(decodeUnicode(o.get('gmnotes'))).split(/(?:[\n\r]+|<br\/?>)/),(l)=>regex.test(l)).join('\r');                               sendChat(o.get('name'),`/w gm ` + lines);                           } else {                       sendChat(o.get('name'),'/w gm &{template:5e-shaped}{{title=' + o.get('name') +'}} {{text='+ decodeURIComponent(decodeUnicode(o.get('gmnotes')))+'}}');                           }                       });               }           });       }); I'm assuming I have to depart at the emboldened line, (which sets up some kind of reference to the token?) and use that so that the properties I am pulling later on come from the character it represents (assuming it does indeed represent a character). But there is soooo much ignorance getting in my way. Is "o.get" the same as "object.get" for instance? What does the "s." do in "getObj('graphic',s._id))"?
1524538673
The Aaron
Pro
API Scripter
.map() applies a function to every element in a collection and returns a new collection of the same length containing the results. So, given the array [a,b,c] and function f(), map([a,b,c], f) is the same as [f(a), f(b), f(c)].  With _.chain(), the collection is always the output of the previous step, so the emboldened line’s map()’s collection is msg.selected and the following line’s collection is the result of applying the emboldened line’s function argument to it. That function is: s => getObj(‘graphic’, s._id) where s is each element of msg.selected, an object that contains the ids of the selected objects in a property named _id.  The purpose of that emboldened line is to get all the graphic objects for the selected things. In the event that some of the selected things are not graphics, getObj() will return undefined. The next line then takes this collection of graphic objects and undefined and gets rid of all the undefined entries. At that point, you have the collection of tokens that are selected.  So, then you just need to inject a new copy of those two lines which first turns the collection into character objects and undefined by applying this function to each entry of the collection: t => getObj(‘character’,t.get(‘represents’)) then get rid of the undefined entries.  That should get you attached character gmnotes... except I’m remembering a small complication. Try changing “gmnotes” to “name” just to verify it works to get the characters. I’ll look up that complication and post back while my arm recovers from typing all this on my phone whilst laying in bed. =D. (Be sure to fix the quotes if you copy/paste the code I typed (silly “pretty” quotes))
1524539638
The Aaron
Pro
API Scripter
Ok, the complication is that gmnotes, bio, and notes (on handouts) must be accessed via the asynchronous .get(). What that means is that you can’t just do: .map( c => c.get(“gmnotes”) ) to convert the collection to a collection of gmnotes entries. That’s actually fine here because you’re dealing with the singly anyway (if you were concatinatimg them, it would be much more complicated!). Instead you have to pass a function as the second parameter to .get(), which will be called with the value of gmnotes: .each( c => c.get(“gmnotes”, (val) => { // do something with val sendChat(“”,val); })) probably you can take everything being done in that trailing .each call in the original script and use it in that callback. =D I’m upgrading this task from “very minor” to “minimal”. =D. Let me know if you hit any snags!
1524545228
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Will I need another ._chain() for mapping t? What is "val"? A value returned by...? I'm making more of a mess than anything, really. I've gotten it not to crash the API by the simple expedience of getting it not to do anything... Sorry, any time you want to put me out of your misery, is fine. There are some fundamental things I'm not grasping, and you have been more than generous with your time. :)
1524575462
The Aaron
Pro
API Scripter
No problem.&nbsp; I give you CharNotes: on('ready',function(){ &nbsp; &nbsp; 'use strict'; &nbsp; &nbsp; const decodeUnicode = (str) =&gt; str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=&gt;String.fromCharCode(parseInt(m.slice(2),16))); &nbsp; &nbsp; on('chat:message',function(msg){ &nbsp; &nbsp; &nbsp; &nbsp; if('api' === msg.type && msg.content.match( /^!c(gmnotes|bio)/ ) && playerIsGM(msg.playerid) ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let match=msg.content.match( /^!c(gmnotes|bio)(?:-(.*))?$/ ), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; where = match[1], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; regex; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(match && match[2] ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; regex = new RegExp(`^${ match[2] }`,'i'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.chain(msg.selected) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( s =&gt; getObj('graphic',s._id)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject(_.isUndefined) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( t =&gt; getObj('character', t.get('represents'))) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject(_.isUndefined) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .each( c =&gt; c.get(where, (val) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(null !== val){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(regex){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let lines=_.filter( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decodeUnicode(val).split(/(?:[\n\r]+|&lt;br\/?&gt;)/), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (l)=&gt;regex.test(l .replace(/&lt;[^&gt;]*&gt; /g,'')) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ).join('\r'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(c.get('name'),`/w gm ` + lines); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(c.get('name'),'/w gm &{template:5e-shaped}{{title=' + c.get('name') +'}} {{text='+ decodeUnicode(val) +'}}'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); }); Works just like GMNote, but with these commands: !cbio !cgmnotes !cbio-Some Line Beginning !cgmnotes-Some Line Beginning Note that I changed it to gmnotes with an s.&nbsp; If that's too annoying, I did that so I could just reuse the part of the command that tells which field to look at, but I can append an s in the code if that breaks your muscle memory. I bolded all the places I changed above, here's a breakdown: I changed the Regular Expression to match the command and the related regular expression to grab the part to match (which now matches the field, too): /^!c(gmnotes|bio)/ /^!c(gmnotes|bio)(?:-(.*))?$/ ^ means start of the input. !c are literally !c (gmnotes|bio) matches either gmnotes or bio , and is a capturing group.&nbsp; Whatever is in here will be stored at index 1 in the match. (?: ) &nbsp;is a "non-capturing group" which means everything inside it is treated as a single unit for the trailing ? (see below) - in the non-capturing group is just a listeral - (.*) is a capturing group that matches .* , which means any character ( . ) repeated 0 or more times ( * ).&nbsp; Whatever this matches will be stored at index 2. ? means 0 or 1 of the preceding item, the non-capturing group above. (You can play with these regular expressions here:&nbsp; <a href="https://regex101.com/r/X4scRZ/1" rel="nofollow">https://regex101.com/r/X4scRZ/1</a>) The first Regular Expression determines if the command for this script was provided.&nbsp; The second is used to extract 2 bits of information.&nbsp; Which field to extract is stored at index 1 and then set into the variable where , what to match is stored at index 2 and is used to construct a regular expression on line 13 if it's present. Next there is a map and reject on lines 19-20 which convert the collection of tokens into a collection of characters. On line 21, I call the asynchronous .get() to extract whichever field is stored in where and pass it a callback function where the result will be passed in as the argument val . Line 22 checks if val is null .&nbsp; Instead of an empty string, the gmnotes, bio, and handout notes fields will be passed back as null .&nbsp; This is one of several inconsistencies in the API that are a result of backend implementation showing through. Lines 24-27 I just broke that operation onto multiple lines as I made some changes to it. Line 25, I removed the decodeURIComponent() call as the text in these 3 fields is stored differently than on the token GMNotes and that decoding was not needed. Line 26, I added in a replace to strip HTML for the by-line match. On line 30, I striped out another call to decodeURIComponent() . Cheers!
I'm really sorry, this must be me, but the GM notes only sometimes works for tokens I have created - I am using it to store information for rooms. I have set up a token with GM notes: The token is formatted using token mod: !token-mod --set tint_color|transparent aura1_color|FF0000 aura1_radius|0.5 isdrawing|yes The macro i use to call the scrips is: /w gm &{template:5e-shaped} {{title=See Notes}} {{text_big=[See GM Notes](!cgmnotes)|[See Bio](!cbio)}} but it will not output to chat for some reason? I am sure this is me in a sleep deprived state missing something.
1524593512

Edited 1524593536
The Aaron
Pro
API Scripter
The above script, !cgmnote s and !cbio pull from the character sheet.&nbsp; For the token's GM Notes, use the script further up that Keith posted with the command !gmnotes : on('ready',function(){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'use strict'; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const decodeUnicode = (str) =&gt; str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=&gt;String.fromCharCode(parseInt(m.slice(2),16))); &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; on('chat:message',function(msg){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let match=msg.content.match(/^!gmnote-(.*)$/), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; regex; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(match && match[1]){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; regex = new RegExp(`^${match[1]}`,'i'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.chain(msg.selected) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( s =&gt; getObj('graphic',s._id)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject(_.isUndefined) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject((o)=&gt;o.get('gmnotes').length===0) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .each( o =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(regex){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let lines=_.filter(decodeURIComponent(decodeUnicode(o.get('gmnotes'))).split(/(?:[\n\r]+|&lt;br\/?&gt;)/),(l)=&gt;regex.test(l)).join('\r'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(o.get('name'),`/w gm ` + lines); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(o.get('name'),'/w gm &{template:5e-shaped}{{title=' + o.get('name') +'}} {{text='+ decodeURIComponent(decodeUnicode(o.get('gmnotes')))+'}}'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; });
ah fair enough, for some reason, I thought it superseded it. thanks as ever Aaron
1524595964
The Aaron
Pro
API Scripter
no prob! =D