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 .
×

API Script: Write character current health, max health and ac to external journal

1559237294

Edited 1559255709
Hey, I have been working on a script lately to write all the characters in the games current health, max health and ac for a streamer. I had it working flawlessly and foolishly tried to add an initiative aspect to it without saving the previously working script. Long story short, the initiative aspect broke writing to the external journal and gave many Max Range Errors. I attempted to use the settimeout function and that fixed the Max Range Error but it made the data overwrite the second characters name in the external journal which was weird. So, I tried to go back by just removing the aspects of the initiative that broke the script and now I am getting a bunch of undefined split errors. (See attached image) Anyone mind helping me get back to my previous point? The script is located here&nbsp; <a href="https://pastebin.com/dUwN6VXD" rel="nofollow">https://pastebin.com/dUwN6VXD</a> Edit: As a side note I actually had a script listening (using selenium) and writing the values to individual files on my computer which had 1 value per file for OBS use on Twitch. Edit2: Posted correct image
1559251990
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I'd recommend disabling all other scripts. Then the line number reference in the error will be correct to the script. Right now it references lines 184, character 41, but line 184 of the script doesn't have a .split.
Sorry about that. I updated the image now it should be line 191. I added comments to the pastebin after I took the screenshot to make it easier to follow and forgot to rerun the script for the correct error code line!
1559256295

Edited 1559256334
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Well this isn't a problem with interacting with the initiative order, your charlist apparently doesn't have an index 4 entry.
I don't see how it doesn't have a fourth index is the thing. I have since been tinkering with it and moving delimiters around and/or changing them so it is a strong possibility that I have either deleted or moved something which has broken it. I will probably work on it more later tonight and probably break it down to one character with one attribute and then build it up from there.
1559261999
The Aaron
Roll20 Production Team
API Scripter
I would rewrite this like so: on('ready',()=&gt;{ const HandoutName = 'Char Stat Dump'; const WriteAttrs = { hp: { curr_hp: 'current', max_hp: 'max' }, ac: { ac: 'current' } }; let OBSData = {}; const assureCharacter = (key) =&gt; (OBSData[key]=OBSData[key]||{}); const assureHandout = () =&gt; { return findObjs({ type: 'handout', archived: false, name: HandoutName })[0] || createObj('handout',{ name: HandoutName }); }; let OBSHandout = assureHandout(); const isPlayerCharacter = (character) =&gt; { if(character){ let players = character.get('controlledby').split(/,/).filter(s=&gt;s.length); return players.includes('all') || (players.filter((p)=&gt;!playerIsGM(p)).length&gt;0); } return false; }; const isPlayerToken = (token) =&gt; { let players = token.get('controlledby') .split(/,/) .filter(s=&gt;s.length); if( players.includes('all') || players.filter((p)=&gt;!playerIsGM(p)).length ) { return true; } if('' !== token.get('represents') ) { return isPlayerCharacter(getObj('character',token.get('represents'))); } return false; }; const writeOBSData = ()=&gt;{ if(OBSHandout){ let j = JSON.stringify(OBSData); OBSHandout.set({ notes: j, gmnotes: j }); } }; const loadAttrDataFromAttr = (charKey, attr) =&gt; { let attrName=attr.get('name'); Object.keys(WriteAttrs[attrName]).forEach(attrKey =&gt; { OBSData[charKey][attrKey]=attr.get(WriteAttrs[attrName][attrKey]); }); }; const loadAttrData = (charKey, charID, attrName) =&gt; { let attr = findObjs({ type: 'attribute', name: attrName, characterid: charID })[0]; if(attr){ loadAttrDataFromAttr(charKey, attr); } }; const loadCharacterData = (character) =&gt; { let key = character.get('name'); assureCharacter(key); Object.keys(WriteAttrs).forEach(a=&gt;loadAttrData(key,character.id,a)); }; const handleChangeAttribute = (obj,prev) =&gt; { if(WriteAttrs.hasOwnProperty(prev.name)){ let c = getObj('character',prev._characterid); if(c &amp;&amp; isPlayerCharacter(c)){ let key = c.get('name'); assureCharacter(key); loadAttrDataFromAttr(key,obj); writeOBSData(); } } }; const handleChangeCharacter = (obj,prev) =&gt; { if(isPlayerCharacter(obj)){ // handle name change if(obj.get('name') != prev.name){ OBSData[obj.get('name')]=OBSData[prev.name]; delete OBSData[prev.name]; writeOBSData(); } else if( !OBSData.hasOwnProperty(obj.get('name'))){ loadCharacterData(obj); writeOBSData(); } } else { if(OBSData.hasOwnProperty(obj.get('name'))){ delete OBSData[obj.get('name')]; writeOBSData(); } } }; findObjs({ type: 'character', archived: false }).filter(isPlayerCharacter) .forEach(loadCharacterData); writeOBSData(); on('change:attribute',handleChangeAttribute); on('change:character',handleChangeCharacter); });
1559262947
The Aaron
Roll20 Production Team
API Scripter
This takes advantage of JSON.stringify() to write the data out so you don't need to deal with formatting the characters.&nbsp; It also caches the data in a variable so you don't need to read the existing data to make changes, just update the cache and re-output. It will update the data based on the permissions of the character (if one is turned on or off, it gets added or removed), also handles renames of characters.&nbsp; The encoding of what attributes to write is easily extensible for additional attributes. I'm using "if it's controlled by all or a player that's not a GM" to determine if it's a player character.&nbsp; I left a function in there for tokens in case you extend this for initiative.
1559263663
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Dragon10580 said: I don't see how it doesn't have a fourth index is the thing. I have since been tinkering with it and moving delimiters around and/or changing them so it is a strong possibility that I have either deleted or moved something which has broken it. I will probably work on it more later tonight and probably break it down to one character with one attribute and then build it up from there. Aaron's rewrite above is a good way to do this, but I just wanted to check something. Are you expecting the array to have 4 entries or 5? The way you refer to the "fourth index" has me confused. Arrays are indexed from 0, so charList[4] would actually be the 5th index of the array.
Scott C. said: Dragon10580 said: I don't see how it doesn't have a fourth index is the thing. I have since been tinkering with it and moving delimiters around and/or changing them so it is a strong possibility that I have either deleted or moved something which has broken it. I will probably work on it more later tonight and probably break it down to one character with one attribute and then build it up from there. Aaron's rewrite above is a good way to do this, but I just wanted to check something. Are you expecting the array to have 4 entries or 5? The way you refer to the "fourth index" has me confused. Arrays are indexed from 0, so charList[4] would actually be the 5th index of the array. Sorry, I did word that very confusingly. So as it stands there are 5 characters which becomes the 4th array index. So there most certainly should be an indexed position here. When I say fourth index I mean charList[4]
The Aaron said: This takes advantage of JSON.stringify() to write the data out so you don't need to deal with formatting the characters.&nbsp; It also caches the data in a variable so you don't need to read the existing data to make changes, just update the cache and re-output. It will update the data based on the permissions of the character (if one is turned on or off, it gets added or removed), also handles renames of characters.&nbsp; The encoding of what attributes to write is easily extensible for additional attributes. I'm using "if it's controlled by all or a player that's not a GM" to determine if it's a player character.&nbsp; I left a function in there for tokens in case you extend this for initiative. Ahh that looks much more efficient! I did not even check to see if JSON.stringify was available to me. I need to brush back up on my JavaScript especially if I am going to try to make anymore scripts for roll20 in my spare time. That cache idea is brilliant!
1559270223
The Aaron
Roll20 Production Team
API Scripter
You shouldn't rely on a fixed number of characters.&nbsp; Reacting to the dynamic number of characters is a better approach.
1559270281
The Aaron
Roll20 Production Team
API Scripter
No problem, happy to help. =D&nbsp; Writing for the API is much more similar to writing a Node Module, then other Javascript pursuits.&nbsp;
Ah that would explain a lot then, I have never dealt with Node Modules. I am much more familiar with JavaScript in the sense of Google Sheets for automation and a bit of AngularJS for progressive web apps. Though I did see a bit of similarity with this API and AngularJS!
1559271580
The Aaron
Roll20 Production Team
API Scripter
Yeah, my understanding is Angular is a lot of Asynchronous operations, similar to Roll20.
Yea, it is. It gets to be a bit crazy dealing with promises and observables and subscriptions to observables. I only worked with it for about 4 months it was an AngularJS and Firebase project for school. Some real crazy stuff! Again I do appreciate all the help Scott and The Aaron! I will tinker around with this more tomorrow!
Update: With the amazing help from The Aaron and Scott I have completed the entire process of getting the sandbox to write to the external journal of the campaign with information of current HP, max HP, AC, and Initiative. Then from there I have a script that utilizes selenium and parses the information!&nbsp; The Aaron, that script is ridiculously elegant I love it!
1559317250
The Aaron
Roll20 Production Team
API Scripter
Sweet!&nbsp; Glad I could help. =D
1559318158
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Well, thanks for the thanks, but Aaron did most of the work here. Glad you've got it working though. It'd be awesome if you would share your completed scripts (both API and the selenium) so that future streamers can use the programs you've developed to handle this for their own streams.
Yea, definitely I will. I need to finish working with this current streamer and get a somewhat decent README.md file and upload it to GitHub to make ease of access a bit easier. Once I get all that I will post here with a link to the GitHub repository unless I need to post that elsewhere in the forums. I am hoping to have that done within a week depending on when I can line up my schedule with the streamer.
1559650954

Edited 1559651105
Alright, I have an update and I am actually getting pretty excited at how versatile Roll20 can be to essentially change how D&amp;D is streamed altogether! So, I won't be able to get it tested probably closer to July but once everything is scrubbed down and cleaned up and tested I will look into making a gist for this. As of right now I have the script pulling Max health, current health, AC, and Initiative thanks to Aaron. I added functionality to pull current level as well. On top of that I have a python script that is scraping that information and setting up text files for each component. It also calculates damage as well for a little bit of added flavor. My next goal is to grab the current map that they are on. I have looked into this and I cannot figure out the most efficient way to do it since at any point in time the group could potentially get separated. I noticed that Map is defined as a 'Graphic' and has a name attribute. Is there a way to verify what page/map the group is on or would an invisible token have to be placed in order 'follow' the group?&nbsp; Edit: I would like to eventually have status incorporated into the script as well!
1559654077
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
For what map they are on, you'll want to look at the campaign function object . Particularly playerpageid and playerspecificpages from the campaign object.
Scott C. said: For what map they are on, you'll want to look at the campaign function object . Particularly playerpageid and playerspecificpages from the campaign object. Ah ok, I was focusing entirely on character and player objects. Thanks for pointing me in the right direction!
1559655120

Edited 1559655139
The Aaron
Roll20 Production Team
API Scripter
Also, note the object relationship: The Campaign has many Pages, which have many Graphics.&nbsp; A Player will be on a given Page. The Page id is what will be in the Campaign playerpageid (where the red ribbon is) or in the playerspecificpages (where a split party member is, which might be the same page as the ribbon). I have some great functions that give you a list of active pages (pages with players on them), and what page players are on, including GMs (which follow different rules). I'll post them when I'm home.&nbsp;
Ah ok that makes sense! The Aaron you are a god among men haha! I am playing around with the API and slowly getting more comfortable with it.
1559669692

Edited 1559669910
The Aaron
Roll20 Production Team
API Scripter
Here are those functions: const getActivePages = () =&gt; [...new Set([ Campaign().get('playerpageid'), ...Object.values(Campaign().get('playerspecificpages')), ...findObjs({ type: 'player', online: true }) .filter((p)=&gt;playerIsGM(p.id)) .map((p)=&gt;p.get('lastpage')) ]) ]; const getPageForPlayer = (playerid) =&gt; { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; getActivePages() returns an array of page ids that players are on.&nbsp; Often, the page id is sufficient and the lowest cost to retrieve.&nbsp; For example, if you only wanted to make updates on Graphics that players can see, you could use the array as a lookup for the pageid on a Graphic without having to load the full Page objects: let pages = getActivePages(); let graphics = getSomeGraphicsSomeHow() &nbsp;&nbsp;&nbsp;&nbsp;.filter( g =&gt; pages.includes(g.get('pageid'))); Something I meant to mention earlier: when referring to the properties of a Roll20 object, you should leave off the leading _.&nbsp; The _ specifies that the property is readonly, but isn't required for the lookup.&nbsp; By leaving off the leading _, you allow your script to work in the future if the property should become readwrite, which has happened a few times.&nbsp; The only exception to this is events.&nbsp; For example, if you want to observe players logging on, you must have the leading _ on _online: on('change:player:_online', ...); in order for the event to trigger. If you need the page objects, it's a simple map operation to load them in: let pageObjs = getActivePages() &nbsp;&nbsp;&nbsp;&nbsp;.map(id =&gt; getObj('page',id)) &nbsp;&nbsp;&nbsp;&nbsp;.filter(p =&gt; undefined !== p); getPageForPlayer() will give you the page id for a given player, which is great for doing things in response to a command they issue. let ppageid = getPageForPlayer(msg.playerid); let playerTokens = findObjs({ &nbsp;&nbsp;&nbsp;&nbsp;type: 'graphic', &nbsp;&nbsp;&nbsp;&nbsp;subtype: 'token', &nbsp;&nbsp;&nbsp;&nbsp;pageid: ppageid }) .filter(t=&gt;playerCanControl(t,msg.playerid)); Freebie since I used it in the example: const playerCanControl = (obj, playerid='any') =&gt; { const playerInControlledByList = (list, playerid) =&gt; list.includes('all') || list.includes(playerid) || ('any'===playerid &amp;&amp; list.length); let players = obj.get('controlledby') .split(/,/) .filter(s=&gt;s.length); if(playerInControlledByList(players,playerid)){ return true; } if('' !== obj.get('represents') ) { players = (getObj('character',obj.get('represents')) || {get: function(){return '';} } ) .get('controlledby').split(/,/) .filter(s=&gt;s.length); return playerInControlledByList(players,playerid); } return false; }; playerCanControl() takes a graphic object, and tells you if a player can control it.&nbsp; With just the graphic, it tells you if any player can control it: let anyPlayerController = playerCanControl(graphic); With a player id, it tells you if that player can control it: let specificPlayerController = playerCanControl(graphic, msg.playerid); or you can specify the special id "all" to see if it is controllable by all players (meaning, assigned the "all players" control, not that every player happens to be on it): let allPlayerController = playerCanControl(graphic, 'all'); Cheers!
Sorry for the late reply, work gave me a headache so I stayed away from the computer! These examples are really helpful! I really appreciate all the help guys!
1559762426
The Aaron
Roll20 Production Team
API Scripter
No problem!