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

[Sheetworkers] Script to add support for setTimeout, setInterval, and async/await.

September 27 (4 years ago)

This has been floating around in the API forum for a few weeks now, but I think it makes since to post here, now...

Ever tried to use setTimeout(), or setInterval() and gotten the following error message, instead?

Character Sheet Error: Trying to do getAttrs when no character is active in sandbox.

Dreaming of a way to escape "callback hell" and start using JavaScript's async/await patterns?

If either of these are true, this script may be for you:

https://github.com/onyxring/Roll20Async 

It has served me well in my own custom character sheets and I'm sharing it with the community in case it helps someone else.

If it doesn't work quite like you expected, feel free to let me know.

-Jim at OnyxRing

October 01 (4 years ago)
Jakob
Sheet Author
API Scripter

This is really nice, thanks for sharing this find! I guess you're not getting much of a response here since most people don't know what to do with it :).

October 01 (4 years ago)
Spren
Sheet Author

Jakob is totally right. Seems cool, but I'm not seeing a particular use case for it. Could one of you give some examples of how you have used it, or would use it?

October 01 (4 years ago)
Andreas J.
Forum Champion
Sheet Author
Translator

Spren said:

Seems cool, but I'm not seeing a particular use case for it. Could one of you give some examples of how you have used it, or would use it?

Seconded. I 100% know I'm too dum-dum to understand how this could be used without an example. :D

October 01 (4 years ago)

Edited October 01 (4 years ago)
Kavini
Pro
Marketplace Creator
Sheet Author
Compendium Curator

If I understand correctly this prevents the endless callback hell of sheetworkers. Instead of using a getAttrs, collecting values, and then being required to nest everything inside that callback function, this would allow you to make the function call to a variable with an await, which you would then use as any other variable, eliminating the need for nesting.

I haven't responded before now, because I haven't had a chance to test it out, but if this works as advertised it'll be wonderful!

October 01 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

I think for most sheet creators, it won't provide a benefit most of the time. It just provides a different syntax, a different way of writing your worker for the same effect.

 BUT, and this is huge, there are some workers it will make a massive difference on. The key areas that occur to me are sheet version update workers, and workers that wont to manipulate more than one repeating section at a time. These often require lots of nesting, and you can write them much cleaner and more understandably with these new functions.

However, you also need to be careful: it makes it easier to write a worker with lots of getAttrs functions (as well as the other worker async functions like setAttrs), so you could easily write workers that end up very inefficient that add lag to your sheet.

October 01 (4 years ago)
Andreas J.
Forum Champion
Sheet Author
Translator

I remember Scot C. have said more than once that either GetAttrs takes ~5ms to process while SetAttr takes ~200ms, or the other way around. Don't remember, but would document it if I did...

October 01 (4 years ago)

Edited October 01 (4 years ago)

Nic B is right.  The async/await approach is really just a (relatively) recent feature of the JavaScript language, which doesn't seem to work in Roll20.  Without async/await/promises, it doesn't mean very much.  Of course, fixing setInterval() and setTimeout is just a bonus, but I've seen it pop up in the forums from time to time.

Truthfully, this code is a chunk of a Bigger release that I was planning on sharing, but it seems like it is self-contained enough to warrant its own posting.

October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter


Andreas J. said:

I remember Scot C. have said more than once that either GetAttrs takes ~5ms to process while SetAttr takes ~200ms, or the other way around. Don't remember, but would document it if I did...

setAttrs is the slower one (much slower). But even the 5ms of getAttrs will still add up when you have one sheet worker that causes a change in another, and another, etc. Of course, most people who use these scripts now will know well enough how to avoid that - but if it becomes popular and there are simple examples to use, people could easily end up writing very slow code, with lots of separate getAttrs calls instead of one unified one. Dont read this as discouraging use of these scripts though - I think they are amazing. Just with any powerful tool, you need to be aware of the possible pitfalls.

On the speed issue, I wonder if Scott did some testing about getSectionIDs. I've often been curious about the speed of that one.


October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

On the subject of examples, here are two simple sheet workers. Note that you dont actually need to use OnyxRings scripts for these - they dont showcase the power of the scripts. They are just meant to show the difference in syntax (and how easy it is to switch to using them):

For the first one: imagine you have a character sheet where the hitpoints attribute is calculated by adding siz and con. The traditional way would be like this:

on('sheet:opened change:con change:siz', ()=>{
    getAttrs(['con','siz'], values => {
        let con = parseInt(values.con) || 0;
        let siz = parseInt(values.siz) || 0;
        let hp = con + siz;
        setAttrs({
            hitpoints: hp
        });
    });
});

The above is a very familiar style of sheet worker. There are more efficient ways to write this, but I wanted to show the most common style of worker.

Notice at each step where you use a roll20 function, you add an extra level of nesting. Here's what that looks like with OnyxRing's scripts:

on('sheet:opened change:con change:siz', async ()=>{
    const values = await getAttrsAsync(['siz','con']);
    const con = +parseInt(values.con) || 0;
    const siz = +parseInt(values.siz) || 0;
    const hp = con + siz;
    setAttrs({
        hitpoints: hp
    });
});

Notice there's no nesting. You write the worker in a simple progression. You just have to add the words async and await at certain points.


Here's a more complex example, working on a repeating section. Imagine you have a repeating section of different types of armour. Each piece has an armor value and an armor worn checkbox. If it's worn you add the armour to the total, and then update a totalarmor attribute. That sheet worker might look like this:

on(`change:repeating_armor`, () => {
    getSectionIDs('repeating_armor', id_array => {
        const armor = [];
        id_array.forEach(id => armor.push(
            `repeating_armor_${id}_armorbonus`,
            `repeating_armor_${id}_armorworn`
        ));
        getAttrs(armor, values => {
            let tarm = 0;
            id_array.forEach(id => {
                const bonus = parseInt(values[`repeating_armor_${id}_armorbonus`]) || 0;
                const worn = parseInt(values[`repeating_armor_${id}_armorworn`]) || 1;
                if(worn) {
                    tarm += bonus;
                }
            });
            setAttrs({
                totalarmor: tarm
            });
        });
    });
});

So, you first need to use getSectionIDs, and then put the rest of the worker inside that. Then use getAttrs, and put the rest of the worker inside that. With OR's scripts, that would look like the code below. It's still pretty complex, but might be more readable since you have a nice linear flow.

on(`change:repeating_armor`, async () => {
    const id_array = await getSectionIDsAsync('repeating_armor');
    const armor = [];
    id_array.forEach(id => armor.push(
        `repeating_armor_${id}_armorbonus`,
        `repeating_armor_${id}_armorworn`
    ));
    const values = await getAttrsAsync(armor);
    let tarm = 0;
    id_array.forEach(id => {
        const bonus = +values[`repeating_armor_${id}_armorbonus`] || 0;
        const worn = +values[`repeating_armor_${id}_armorworn`] || 1;
        if(worn) {
            tarm += bonus;
        }
    });
    setAttrs({
        totalarmor: tarm
    });
});


To really show the power of these scripts though, you need to be using more complex workers - once that use setAttrs and then do things after it, say, or ones which work on multiple repeating sections (I have done one version update script that worked on something like 6 repeating sections, and i needed to nest each one inside the next - being able to just write out a chain of 6 getSectionIDsAsyc in a vertical line would have made a much more readable worker).

But I hope these examples show the basics.

October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here's a more complex example. Imagine you have a system which has spells that go from level 1 to 9, and you have set up a different repeating section for each spell level. I've seen at least one sheet set up this way, so its not an unrealistic example.

Now, the spells need to have a bonus calculated, and that is based on the stats int, wis, or cha. So every time one of those 3 stats change, your workers needs to check all spells to see which spell bonuses have changed. The sheet worker for level 1 spells might look like this:

on(`change:repeating_spells01 change:int change:wis change:cha`, () => {
    const statsallowed = ['int''wis''cha'];
    getSectionIDs('repeating_spells01'id_array => {
        const spells = [];
        id_array.forEach(id => spells.push(`repeating_spells01_${id}_statbonus`));
        getAttrs([...spells, ...statsallowed], values => {
            let output = {};
            id_array.forEach(id => {
                const stat = values[`repeating_spells01_${id}_statbonus`];
                output[`repeating_spells01_${id}_spellmodifier`] = values[stat];
            });
            setAttrs(output);
        });
    });
});

Explaining the forEach loop inside the getAttrs: each spell has a stat linked to it, and that name of that stat is stored in an attribute within the repeating section. Then the worker gets the value of the stat using that name, and stores it in the spellmodifer attribute for that stat.

So far so good. The problem is, you need another worker for each level of spell. These means every time one of the 3 stats, you are doing 9 getAttrs and 9 setAttrs operations. That is very inefficient.

One solution for this would be to convert it to a single worker that does all 9 spells at once. Unfortunately that looks something like this:

on('change:repeating_spells01 change:repeating_spells02 change:repeating_spells03 change:repeating_spells04 change:repeating_spells05' + 'change:repeating_spells06 change:repeating_spells07 change:repeating_spells08 change:repeating_spells09 change:int change:wis change:cha', () => {
    const statsallowed = ['int''wis''cha'];
    const spells = [];
    const output = {};
    getSectionIDs('repeating_spells01'ids_level01 => {
        ids_level01.forEach(id => spells.push(`repeating_spells01_${id}_statbonus`));
        getSectionIDs('repeating_spells02'ids_level02 => {
            ids_level02.forEach(id => spells.push(`repeating_spells02_${id}_statbonus`));
            getSectionIDs('repeating_spells03'ids_level03 => {
                ids_level03.forEach(id => spells.push(`repeating_spells03_${id}_statbonus`));
                getSectionIDs('repeating_spells04'ids_level04 => {
                    ids_level04.forEach(id => spells.push(`repeating_spells04_${id}_statbonus`));
                    getSectionIDs('repeating_spells05'ids_level05 => {
                        ids_level05.forEach(id => spells.push(`repeating_spells05_${id}_statbonus`));
                        getSectionIDs('repeating_spells06'ids_level06 => {
                            ids_level06.forEach(id => spells.push(`repeating_spells06_${id}_statbonus`));
                            getSectionIDs('repeating_spells07'ids_level07 => {
                                ids_level07.forEach(id => spells.push(`repeating_spells07_${id}_statbonus`));
                                getSectionIDs('repeating_spells08'ids_level08 => {
                                    ids_level08.forEach(id => spells.push(`repeating_spells08_${id}_statbonus`));
                                    getSectionIDs('repeating_spells09'ids_level09 => {
                                        ids_level09.forEach(id => spells.push(`repeating_spells09_${id}_statbonus`));
                                        getAttrs([...spells, ...statsallowed], values => {
                                            ids_level01.forEach(id => {
                                                const stat = values[`repeating_spells01_${id}_statbonus`];
                                                output[`repeating_spells01_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level02.forEach(id => {
                                                const stat = values[`repeating_spells02_${id}_statbonus`];
                                                output[`repeating_spells02_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level03.forEach(id => {
                                                const stat = values[`repeating_spells03_${id}_statbonus`];
                                                output[`repeating_spells03_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level04.forEach(id => {
                                                const stat = values[`repeating_spells04_${id}_statbonus`];
                                                output[`repeating_spells04_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level05.forEach(id => {
                                                const stat = values[`repeating_spells05_${id}_statbonus`];
                                                output[`repeating_spells05_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level06.forEach(id => {
                                                const stat = values[`repeating_spells06_${id}_statbonus`];
                                                output[`repeating_spells06_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level07.forEach(id => {
                                                const stat = values[`repeating_spells07_${id}_statbonus`];
                                                output[`repeating_spells07_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level08.forEach(id => {
                                                const stat = values[`repeating_spells08_${id}_statbonus`];
                                                output[`repeating_spells08_${id}_spellmodifier`] = values[stat];
                                            });
                                            ids_level09.forEach(id => {
                                                const stat = values[`repeating_spells09_${id}_statbonus`];
                                                output[`repeating_spells09_${id}_spellmodifier`] = values[stat];
                                            });
                                            setAttrs(output);
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});

Caveat: there are more efficient ways to write this (especially to avoid the 9 forEach loops inside getAttrs) - but you cant avoid doing the 9 getSectionIDs, and thats the main point of this example. 

Writing - and reading - sheet workers like that is no fun. With OnyxRing's scripts, this can be rewritten as:

on('change:repeating_spells01 change:repeating_spells02 change:repeating_spells03 change:repeating_spells04 change:repeating_spells05' + 'change:repeating_spells06 change:repeating_spells07 change:repeating_spells08 change:repeating_spells09 change:int change:wis change:cha'async () => {
    const statsallowed = ['int''wis''cha'];
    const spells = [];
    const output = {};

    const ids_level01 = await getSectionIDsAsync('repeating_spells01');
    const ids_level02 = await getSectionIDsAsync('repeating_spells02');
    const ids_level03 = await getSectionIDsAsync('repeating_spells03');
    const ids_level04 = await getSectionIDsAsync('repeating_spells04');
    const ids_level05 = await getSectionIDsAsync('repeating_spells05');
    const ids_level06 = await getSectionIDsAsync('repeating_spells06');
    const ids_level07 = await getSectionIDsAsync('repeating_spells07');
    const ids_level08 = await getSectionIDsAsync('repeating_spells08');
    const ids_level09 = await getSectionIDsAsync('repeating_spells09');

    ids_level01.forEach(id => spells.push(`repeating_spells01_${id}_statbonus`));
    ids_level02.forEach(id => spells.push(`repeating_spells02_${id}_statbonus`));
    ids_level03.forEach(id => spells.push(`repeating_spells03_${id}_statbonus`));
    ids_level04.forEach(id => spells.push(`repeating_spells04_${id}_statbonus`));
    ids_level05.forEach(id => spells.push(`repeating_spells05_${id}_statbonus`));
    ids_level06.forEach(id => spells.push(`repeating_spells06_${id}_statbonus`));
    ids_level07.forEach(id => spells.push(`repeating_spells07_${id}_statbonus`));
    ids_level08.forEach(id => spells.push(`repeating_spells08_${id}_statbonus`));
    ids_level09.forEach(id => spells.push(`repeating_spells09_${id}_statbonus`));
    
    const values = await getAttrsAsync([...spells, ...statsallowed]);

    ids_level01.forEach(id => {
        const stat = values[`repeating_spells01_${id}_statbonus`];
        output[`repeating_spells01_${id}_spellmodifier`] = values[stat];
    });
    ids_level02.forEach(id => {
        const stat = values[`repeating_spells02_${id}_statbonus`];
        output[`repeating_spells02_${id}_spellmodifier`] = values[stat];
    });
    ids_level03.forEach(id => {
        const stat = values[`repeating_spells03_${id}_statbonus`];
        output[`repeating_spells03_${id}_spellmodifier`] = values[stat];
    });
    ids_level04.forEach(id => {
        const stat = values[`repeating_spells04_${id}_statbonus`];
        output[`repeating_spells04_${id}_spellmodifier`] = values[stat];
    });
    ids_level05.forEach(id => {
        const stat = values[`repeating_spells05_${id}_statbonus`];
        output[`repeating_spells05_${id}_spellmodifier`] = values[stat];
    });
    ids_level06.forEach(id => {
        const stat = values[`repeating_spells06_${id}_statbonus`];
        output[`repeating_spells06_${id}_spellmodifier`] = values[stat];
    });
    ids_level07.forEach(id => {
        const stat = values[`repeating_spells07_${id}_statbonus`];
        output[`repeating_spells07_${id}_spellmodifier`] = values[stat];
    });
    ids_level08.forEach(id => {
        const stat = values[`repeating_spells08_${id}_statbonus`];
        output[`repeating_spells08_${id}_spellmodifier`] = values[stat];
    });
    ids_level09.forEach(id => {
        const stat = values[`repeating_spells09_${id}_statbonus`];
        output[`repeating_spells09_${id}_spellmodifier`] = values[stat];
    });
    setAttrs(output);
});

The 9 forEach loops at the end are still a bit clunky, and that code could be rewritten, but I wanted to avoid any more tricky techniques to keep the workers as simple to understand as possible. 

You should be able to see at a glance though how much more readable this code is compared to the previous worker.

October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

Finally, here's an example from OnyxRings githib:

on("sheet:opened", async ()=>{
    var values = await getAttrsAsync(["hp","rec"]);
    var newHp=Number(values.hp||0) + Number(values.rec||0);
    await setAttrsAsync({hp:newHp});
    values = await getAttrsAsync(["hp"]);
    console.assert(values.hp==newHp, "Failed");
  });

This is calculating a HP score, saving it to the sheet. And then after setAttrs has run to set the value, it checks to see if the value on the sheet has been updated correctly. This kind of thing is a pain to do normally. Wanting to do things after setting sheet values takes you to callback hell. This is going to make sheet versioning sheet workers a lot easier to write.

October 02 (4 years ago)

@GiGs:  Thanks for posting these examples.  It’s pretty clear my initial post on this could have been more thorough in this regard.   

October 02 (4 years ago)
Peter B.
Plus
Sheet Author

GiGs said:

Finally, here's an example from OnyxRings githib:

on("sheet:opened", async ()=>{
    var values = await getAttrsAsync(["hp","rec"]);
    var newHp=Number(values.hp||0) + Number(values.rec||0);
    await setAttrsAsync({hp:newHp});
    values = await getAttrsAsync(["hp"]);
    console.assert(values.hp==newHp, "Failed");
  });

This is calculating a HP score, saving it to the sheet. And then after setAttrs has run to set the value, it checks to see if the value on the sheet has been updated correctly. This kind of thing is a pain to do normally. Wanting to do things after setting sheet values takes you to callback hell. This is going to make sheet versioning sheet workers a lot easier to write.

Is async/await available now in sheets or is it an update that is scheduled to come later for sheets in the future?


October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter


Peter B. said:

Is async/await available now in sheets or is it an update that is scheduled to come later for sheets in the future?

It's a feature added by the functions OnyxRings adds in the OP. It's not a feature roll20 has added, you need these functions to enable them.


October 02 (4 years ago)

Async/await as a syntax is part of the JavaScript language. It’s available to use, but doesn’t work quite right in vanilla Roll20. Instead, without the script, Roll20 generates the error described above. 

October 02 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

Async/Await work fine in the API. It's just in sheet workers where they don't work, and specifically on functions that require a character context - getAttrs, setAttrs, getSectionIDS - exactly the functions that OR replaces here. You can use async/await in your own functions (as OR has here, in fact). I believe its just the previously named functions that interface with roll20's backend that lose the character context if you try to use async/await with them. 

October 02 (4 years ago)
Spren
Sheet Author

I honestly really like this now that I've seen it. Thanks for taking the time and doing this Onyx, and thank you GiGs for the really good explanations and examples.

October 03 (4 years ago)

Good clarification, GiGs.  Thanks!  Spren: I hope this works out for you. 

October 03 (4 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Awesome work Onyx!

GiGs, I didn't do any testing with getSectionIDs, mostly cause I just didn't think of it.

Onyx, a question. In your code, I see a function call to getActiveCharacterId(), but no definition of that function. Is that a function you created? or is it a Roll20 function? It's not documented in the sheetworker docs, so is it a backend function that has been erroneously exposed?


I ask, cause while I'm ecstatic to see async/await/promises come to sheetworkers, I'm actually more excited about the prospect of getting the active character's id. Being able to get the ID would allow me to do some better indexing with the trick that allows pseudo interaction between separate sheets. And, even more exciting, if I understand your code correctly it may actually allow us to manipulate connected sheets when an event occurs on one of them. That's just based on my first pass of reading the code, but if true it would be a game changer for a wide array of systems.

October 03 (4 years ago)

Onyx, a question. In your code, I see a function call to getActiveCharacterId(), but no definition of that function. Is that a function you created? or is it a Roll20 function? It's not documented in the sheetworker docs, so is it a backend function that has been erroneously exposed?

It is a Roll20 function; getActiveCharacterId() is built into the SheetWorker code.  As far as being "erroneously" exposed... It feels  to me like it was exposed with intention.  Still, like you, my Spidey-sense tingles a bit because a couple of the tricks I use are undocumented.  I actually dedicated a sidebar on the topic in the docs for ORAL ORCS, page 16.  (Yeah I know, shameless plug).



October 03 (4 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Huh, thanks for the details Onyx. I'm gonna have to play around with this, but I've got some ideas of ways to use that.

October 03 (4 years ago)

Edited November 30 (1 year ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Ok, so with what Onyx has found and put together here, we can do something via character sheets which has previously been the sole purview of the API. This alone may make Roll20 close these functions off, but here goes.

Because we can now get the active character's ID, and change what the active character is (by changing the id of the active character), we can now affect other sheets based on a change on another sheet:


Those eagle eyed watchers will note that to clear the received field, I actually put a space in. This is because the attribute backed span that I'm using for the received value doesn't clear when set to "".
Here's the code I'm using to do this (including Onyx's original Roll20Async code):

<input type='text' title='@{character_id}' name='attr_character_id' value=''>
<input type='text' title='@{target_id}' name='attr_target_id' value=''>
<input type='text' title='@{shared_value}' name='attr_shared_value' value=''>
<span>Received Value:</span><input type='readonly' name='attr_received_value'>
<script type='text/worker'>
    function setActiveCharacterId(charId){
        var oldAcid=getActiveCharacterId();
        var ev = new CustomEvent("message");
        ev.data={"id":"0", "type":"setActiveCharacter", "data":charId};
        self.dispatchEvent(ev); 
        return oldAcid;
    };
    var _sIn=setInterval;
    setInterval=function(callback, timeout){
        var acid=getActiveCharacterId();
        _sIn(
            function(){
                var prevAcid=setActiveCharacterId(acid);
                callback();
                setActiveCharacterId(prevAcid);
            }
        ,timeout);
    };
    var _sto=setTimeout
    setTimeout=function(callback, timeout){
        var acid=getActiveCharacterId();
        _sto(
            function(){
                var prevAcid=setActiveCharacterId(acid);
                callback();
                setActiveCharacterId(prevAcid);
            }
        ,timeout);
    };
    function getAttrsAsync(props){
        var acid=getActiveCharacterId(); //save the current activeCharacterID in case it has changed when the promise runs 
        var prevAcid=null;               //local variable defined here, because it needs to be shared across the promise callbacks defined below
        return new Promise((resolve,reject)=>{
                prevAcid=setActiveCharacterId(acid);  //in case the activeCharacterId has changed, restore it to what we were expecting and save the current value to restore later
                try{
                    getAttrs(props,(values)=>{  resolve(values); }); 
                }
                catch{ reject(); }
        }).finally(()=>{
            setActiveCharacterId(prevAcid); //restore activeCharcterId to what it was when the promise first ran
        });
    };
    //use the same pattern for each of the following...
    function setAttrsAsync(propObj, options){
        var acid=getActiveCharacterId(); 
        var prevAcid=null;               
        return new Promise((resolve,reject)=>{
                prevAcid=setActiveCharacterId(acid);  
                try{
                    setAttrs(propObj,options,(values)=>{ resolve(values); });
                }
                catch{ reject(); }
        }).finally(()=>{
            setActiveCharacterId(prevAcid); 
        });
    };
    function getSectionIDsAsync(sectionName){
        var acid=getActiveCharacterId(); 
        var prevAcid=null;               
        return new Promise((resolve,reject)=>{
                prevAcid=setActiveCharacterId(acid);  
                try{
                    getSectionIDs(sectionName,(values)=>{ resolve(values); });
                }
                catch{ reject(); }
        }).finally(()=>{
            setActiveCharacterId(prevAcid); 
        });
    };
    function log(msg,obj){
        const sheetName = 'Test Sheet';
        if(typeof msg === 'string'){
            console.log(`%c${sheetName} log| ${msg}`,"background-color:#159ccf");
        }else{
            console.log(`%c${sheetName} log| ${typeof msg}`,"background-color:#159ccf");
            console.log(msg);
        }
        if(obj){
            console.log(obj);
            console.log(`%c==============`,"background-color:#159ccf");
        }
    };
    on('sheet:opened',()=>{
        const setObj = {};
        setObj.character_id = getActiveCharacterId();
        setAttrs(setObj);
    });
    on('change:shared_value',async (event)=>{
        log(event);
        let attributes = await getAttrsAsync(['character_id','target_id']);
        if(!attributes.target_id || attributes.target_id === ''){
            return;
        }
        const sourceID = attributes.character_id,
            setObj ={};
        let targetID = setActiveCharacterId(attributes.target_id);
        setObj.received_value =event.newValue === undefined ? '' : event.newValue;
        log(setObj);
        await setAttrsAsync(setObj);
        setActiveCharacterId(sourceID);
    });
    log('sheetworker loaded');
</script>

So, thanks for this Onyx!

October 03 (4 years ago)
GiGs
Pro
Sheet Author
API Scripter

Intriguing. i also noticed that getActiveCharacterId function and wondered if we'd be able to do something like this.

October 04 (4 years ago)

Edited October 05 (4 years ago)

@scott:  this is really cool.  I particularly like how you demoed it as GIF, which really drives home what it can do!

October 05 (4 years ago)
Jakob
Sheet Author
API Scripter

And thus get/setActiveCharacterId became part of the public API :).