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] How would I pull info from a character sheet and send it to another program?

Hey there, firstly I'm sorry if this is the wrong place to post this, or if this is just straight up impossible!

I'm wanting to pull numbers from my players character sheets (using FFG's Genesys system) specifically Wounds and Strain and send that info to another program. I have very little experience with this kind of thing so I'm not really sure where I should get started, any help would be greatly appreciated, thanks in advance!

June 25 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

There's no native way to do this in roll20 - it is in fact set up to make this as hard as possible.

What program are you trying to send the information to, and how do you intend to use it?

June 25 (5 years ago)

Edited June 25 (5 years ago)

I want it to use the numbers for a OBS overlay so it can show live numbers of the players health status. I had figured that I could send the information from Roll20 to a notepad and use OBS to pick it up from that. 

Alternatively could I use a site like glitch to store or pickup the info somehow?

June 25 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

I believe someone was once working on an OBS plugin, but they either never shared it or never completed it.

Some of the information in your campaign can be made sort-of public through the External Journal. You would probably need to write a web parser to scrape the information from there. I'm not too familiar with OBS, so not sure how to get it into there.

Another option, working entirely within roll20: would be to create a handout, have a script update that handout with the stats you want to display, whenever the stats change. Then create a second roll20 account, and in that account have the handout in view. Get that view setup as a floating overlay window in OBS.

If you want something more complicated, the web scraper might be the only way and that's well outside my expertise, so i dont know how practical it is. The fact that people have discussed it several times over the years, but no public solution has emerged, suggests its likely very tricky indeed (or needs a very specialised skillset).

June 25 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

I think most people do this with a combination of an API script that writes to a handout and a url scraper that loads that handout from the external journal. 


GiGs said:

Another option, working entirely within roll20: would be to create a handout, have a script update that handout with the stats you want to display, whenever the stats change.

Is this relatively simple to do? Would you be able to point me in the right direction as to where I would get started on this? This falls very much inline with my original idea of just capturing the character sheet or token, just like waaay more sophisticated!



The Aaron said:

I think most people do this with a combination of an API script that writes to a handout and a url scraper that loads that handout from the external journal. 

 Is this the same thing that GiGs is referring to or an alternative solution? Is it "easily" accomplished or would it take a lot of prior knowledge/ experience, and if you don't mind do you know where I would get started on working something like this out?

June 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Same thing as GiGs.  Not terribly difficult if you're writing it in a specific campaign.  A little complicated if you're writing a general purpose script for anyone to use.

I'm trying to use this code  (from this post) as a base for figuring it out myself, though I am having some trouble with it. I have now moved on to trying to re learn javascript, and am wondering if this code is a good representation for what I'm trying to accomplish.

Thank you for your help thus far! 

June 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Very inefficient, but on the right track.

Hahah okay thank you! Wait until you see what I end up with, it'll give a new meaning to the word inefficient!

June 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Minorly rewritten to be more efficient (dropped all the if/else for character IDs):

on("ready", function() {
    const CharIDs = [];

    //Fetch current hp/max hp/ac/level for each character and save to "Char Stat Dump" handout 
    //x,x,x;y,y,y;z,z,z;a,a,a;
    let characters=findObjs({_type: 'character'});
    let attributes=findObjs({_type: 'attribute'});
    let formattedString="";
    characters.forEach(function (chr) {
        if(chr.get("inplayerjournals").split(",").length<=1){
            return;
        }
        if(formattedString!=""){
            formattedString+=";";
        } 
        attributes.forEach(function (attr) {
            if(attr.get('_characterid')==chr.get('_id')){
                if(attr.get('name')=="hp"){
                    formattedString+=attr.get('current')+","+attr.get('max');
                }
                else if(attr.get('name')=="ac"){
                    formattedString+=attr.get('current')+",";
                }
                else{
                    //Do nothing, not really needed but might be good for later
                }       
            }
        });
        let handout=findObjs({_type: "handout", name: "Char Stat Dump"})[0];
        handout.set("gmnotes",formattedString);
    });

    on("change:attribute", function(obj) {
        let curChar=getObj("character", obj.get("_characterid"));//type, unique id
        let playerIDs=curChar.get("inplayerjournals").split(",");
        if(playerIDs.length>1) {
            //Fetch current hp/max hp/ac for each character and save to "Char Stat Dump" handout in some formatted fashion
            //x,x,x;y,y,y;z,z,z;a,a,a;
            //Order fetched is always the same
            let handout=findObjs({_type: "handout", name: "Char Stat Dump"})[0];
            let formattedString="";
            handout.get("gmnotes",function(notes){
                let charList=notes.split(";");

                if(CharIDs.includes(curChar.get("id"))){
                    let statList=charList[0].split(',');//ac, current hp, max hp
                    if(obj.get('name')=="hp"){
                        statList[1]=obj.get('current');
                        statList[2]=obj.get('max');
                    }
                    if(obj.get('name')=="ac"){
                        statList[0]=obj.get('current');
                    }
                    charList[0]=statList.join(',');
                }
                formattedString=charList.join(';');
                handout.set('gmnotes',formattedString);
            });
        }
    });
});


June 26 (5 years ago)

Edited June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here are some suggestions based on what I think you're doing. To clarify: i think you are trying to get a set of specific attribute values from a specific set of characters, and arrange them in a handout for viewing.

In this case, you dont ever need to read the contents of a handout. Just read the values of the characters, build the string, and then replace the contents of the handout with the new values.

This approach lets you create a function do do the work, and then you can call that same function in the on(ready) and in the on(change) sections.


Looking at the start of your function, you have a findObjs that calls all characters, and another that calls all attributes.

It would be better to first establish the list of characters you want - assuming its just the PCs, either hardcoded them in the function and manual edited when needed, or saved them in state and have a function to update the list). 

Likewise, have a list of attribute names.

Then you can use findObjs to grab only the characters you need, then only the attributes on those characters that you need. You dont need to hardcode a specific number of characters, you can use a loop to go through all the characters in the list, which will work for any number of characters.


Edit: lol, of course Aaron would write the script in time it takes for me to offer suggestions for it.

June 26 (5 years ago)

Edited June 26 (5 years ago)

Thank you guys both so much, have been insanely helpful!

If I understand it correctly, what Aaron edited isn't what GiGs was suggesting? My guess here is that the code Aaron edited is more versatile, while the suggestions GiGs made is rigid but would work because the thing I'm trying to do isn't complex?

And thank you Aaron I will use your edit as my base for figuring it out!!

Just to clarify I have literally no idea what I'm doing, I had to test it several times before I was sure I needed on('ready')

June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter


Matt said:

If I understand it correctly, what Aaron edited isn't what GiGs was suggesting? My guess here is that the code Aaron edited is more versatile, while the suggestions GiGs made is rigid but would work because the thing I'm trying to do isn't complex?

I dont know about more versatile. It depends on your definition. I think Aaron was just cleaning up the base code a bit, not necessarily making the best code he could. 

That approach has potential drawbacks IMO, because it assumes every character that has a player in the visibility section is an active character that you want to display. That would be rarely true in my games (my players often have more than one character, some that aren't active every session, and there are NPCs that might be visible to them). But if it is true for your game, it's a very smooth way to get all the characters.


I'd say mine is more flexible, since you have full control over which characters get displayed, though if you go the hardcoded names route, it's certainly a bit clunkier to set up.  

It might also be more efficient, since you dont need to loop through every character and every attribute in the campaign (that probably doesnt have any real effect in performance - though with recent lag, who knows). More importantly you dont have to repeat much of the same code twice in the script. 

But if the supplied code works, and you dont have to write new code, that's the best way to go :)

June 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Yeah, I just corrected the most egregious problem in the original, didn't do much else to it.  It's probably an ok starting point but I can't say much more about it.

June 26 (5 years ago)

Edited June 26 (5 years ago)

So I did some very minor editing to the code Aaron edited, and for some reason that I've spent the last hour trying to figure out why it's slightly broken.

It outputs all the information into the handout perfectly, but only x updates live the others only update when I reset the sandbox. The weirder part is that all four character sheets are able to update x live, it seems x takes precedent over x when the sandbox resets but afterwards it's the most recent attribute that was modified that takes precedent. 

Any help would again be very greatly appreciated!!

on("ready", function() {
    const CharIDs = ['-MAhCsI9sql6KlDA6Mrz','-MAj1gtN4mNi0S8Xesbq','-MAjU5eFIGVRoBEl2mOS','-MAjTcUApOs8VZcc8ALa'];

    //Fetch current wounds & strain for each character
    //x,x,|y,y,|z,z,|a,a,|
    let characters=findObjs({_type: 'character'});
    let attributes=findObjs({_type: 'attribute'});
    let formattedString="";
    characters.forEach(function (chr) {
        if(chr.get("inplayerjournals").split(",").length<=1){
            return;
        }
        if(formattedString!=""){
            formattedString+="|";
        } 
        attributes.forEach(function (attr) {
            if(attr.get('_characterid')==chr.get('_id')){
                if(attr.get('name')=="wounds"){
                    formattedString+=attr.get('current')+",";
                }
                else if(attr.get('name')=="strain"){
                    formattedString+=attr.get('current')+",";
                }
                else{
                    //Do nothing, not really needed but might be good for later
                }       
            }
        });
        let handout=findObjs({_type: "handout", name: "WSOutput"})[0];
        handout.set("notes",formattedString);
    });

    on("change:attribute", function(obj) {
        let curChar=getObj("character", obj.get("_characterid"));//type, unique id
        let playerIDs=curChar.get("inplayerjournals").split(",");
        if(playerIDs.length>1) {
            //Fetch current wounds & strain for each character and save to "Char Stat Dump" handout in some formatted fashion
            //x,x,|y,y,|z,z,|a,a,|
            //Order fetched is always the same
            let handout=findObjs({_type: "handout", name: "WSOutput"})[0];
            let formattedString="";
            handout.get("notes",function(notes){
                let charList=notes.split("|");

                if(CharIDs.includes(curChar.get("id"))){
                    let statList=charList[0].split(',');//wounds & strain
                    if(obj.get('name')=="wounds"){
                        statList[0]=obj.get('current');
                    }
                    if(obj.get('name')=="strain"){
                        statList[1]=obj.get('current');
                    }
                    charList[0]=statList.join(',');
                }
                formattedString=charList.join('|');
                handout.set('notes',formattedString);
            });
        }
    });
});
June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

When you describe the problem, you refer to x a lot. What does x represent?

June 26 (5 years ago)

Edited June 26 (5 years ago)

Oh sorry!! X is the first number set that get's printed out it's x,x,y,y,z,z,a,a. 

So y,z,a all update x live. 

I suppose x is also character sheet x (which is supposed to print to number set x) and the same for y,z,a.

June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

This is your problem

let statList=charList[0].split(',');

It looks like you should have a loop there, looping through the characters - but its always using the first character.

Alright I'm staring at js tutorials on loops, but I'm not seeing how I would integrate them here. How would you suggest doing that?

June 26 (5 years ago)

Edited June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here's a streamlined version of the function. Since you have the four charIDs at the start, you dont need to use the inplayerjournals.

on('ready', function() {
    const charIDs = ['-MAhCsI9sql6KlDA6Mrz','-MAj1gtN4mNi0S8Xesbq','-MAjU5eFIGVRoBEl2mOS','-MAjTcUApOs8VZcc8ALa'];
    const attrNames = ['wounds', 'strain'];
    const handoutName = 'WSOutput';
    const buildNotes =() => {
        let formattedString=[];
        //Fetch current wounds & strain for each character
        //x,x,|y,y,|z,z,|a,a,|
        charIDs.forEach(function (chr) {
            let attrStrings = [];
            attrNames.forEach(function (attr) {
                attrStrings.push(getAttrByName(chr, attr, 'current'));
            });
            formattedString.push(attrStrings.join(','));
        });
        
        let handout=findObjs({_type: 'handout', name: handoutName})[0];
        handout.set('notes',formattedString.join('|'));    
    };
    buildNotes();

    on('change:attribute', function(obj) {
        if(attrNames.includes(obj.get('name')) && charIDs.includes(obj.get('_characterid'))) {
            buildNotes();
        }
    });
});

If you choose to add any extra attributes, you can just add them to the array at the start and they'll automatically be included.

It literally astounds me how fast you've done that, and it just works, I wasn't so happy I might cry. Seriously though thank you so much, I have spent most of the day on this (and I'm pretty sure this is the easy part of my problem), you have been more help than I can describe!!! It also amazes me that there are two programs that do the same thing but one is double the size of the other.

Thank you both Aaron and GiGs very much, I'm going to go drive myself crazy trying to figure out how to scrape the url or whatever. Have a blessed day and or night the both of you!!!!   

June 26 (5 years ago)

Edited June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

And here's a version formatted as a table (you can resize the handout and the table will shrink or grow)

on('ready', function() {
    const charIDs = ['-MAhCsI9sql6KlDA6Mrz','-MAj1gtN4mNi0S8Xesbq','-MAjU5eFIGVRoBEl2mOS','-MAjTcUApOs8VZcc8ALa'];
    const attrNames = ['wounds', 'strain'];
    const handoutName = 'WSOutput';
    
    const buildNotes =() => {
        let formattedString=[];
        let str = [''];
        attrNames.forEach(attr => str.push(attr[0].toUpperCase() + attr.slice(1)));
        formattedString.push('<td>' + str.join('</td><td>') + '</td>');
        //Fetch current wounds & strain for each character
        //x,x,|y,y,|z,z,|a,a,|
        charIDs.forEach(function (chr) {
            let attrStrings = [];
            attrNames.forEach(function (attr) {
                attrStrings.push(getAttrByName(chr, attr, 'current'));
            });
            const charName = getObj('character', chr).get('name');
            formattedString.push('<td>' + charName + '</td><td>' + attrStrings.join('</td><td>') + '</td>');
        });
        
        let handout=findObjs({_type: 'handout', name: handoutName})[0];
        handout.set('notes','<table><tr>' + formattedString.join('</tr><tr>') + '</tr></table>');    
    };
    buildNotes();

    on('change:attribute', function(obj) {
        if(attrNames.includes(obj.get('name')) && charIDs.includes(obj.get('_characterid'))) {
            buildNotes();
        }
    });
});
June 26 (5 years ago)

Edited June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter


Matt said:

It literally astounds me how fast you've done that, and it just works, I wasn't so happy I might cry. Seriously though thank you so much, I have spent most of the day on this (and I'm pretty sure this is the easy part of my problem), you have been more help than I can describe!!! It also amazes me that there are two programs that do the same thing but one is double the size of the other.

Thank you both Aaron and GiGs very much, I'm going to go drive myself crazy trying to figure out how to scrape the url or whatever. Have a blessed day and or night the both of you!!!!   


aw shucks, thanks!

I could have written it much more compactly, but I wanted to leave it fairly understandable. Good luck with the scraper!

If you cant get a working scraper, the second script i posted might work as one of those OBS overlay thingies. It might not be pretty, but i think you can make the background transparent, so it could work.

Oh my god..

It has their names in it, holy crap dude you are God. Thank you!

June 26 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

You're welcome :)

I just tweaked the table version so it capitalises the first letter of the attribute names in the headings. That makes it look a little prettier.