├── .gitignore ├── README.md ├── orcsAsync.js └── orcsAsync.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ___ 2 | **_Note:_** 3 | Nov 2023 - This repository as been renamed from Roll20Async to orcsAsync, in order to align it under the OnyxRing Client Scripts (ORCS) umbrella. 4 | ___ 5 | # orcsAsync 6 | Adds support for asynchronous code in Roll20 Sheetworkers 7 | 8 | ### What this script is addressing: 9 | In the vanilla version of the Roll20 platform, Sheetworkers lose the character's context with asynchronous callbacks. For example, the following straightforward code *should* log a character's STR attribute every ten seconds: 10 | 11 | ``` 12 | on("sheet:opened", ()=>{ 13 | setInterval(()=>{ 14 | getAttrs(["str"],(vals)=>{ 15 | log(vals.str); 16 | }); 17 | },10000); 18 | }); 19 | ``` 20 | 21 | Instead, unfixed, it produces the following error message every ten seconds: 22 | 23 | >Character Sheet Error: Trying to do getAttrs when no character is active in sandbox. 24 | 25 | The same error happens when trying to access attributes within the `setTimeout()` callback, as well as any attempt to wrap attribute access into Async/Await/Promise patterns. 26 | 27 | ### Installation 28 | To use this script in your own SheetWorkers, insert either the orcsAsync.js file, or the minimized version of that file (orcsAsync.min.js), into the top of your Sheetworker. 29 | 30 | ### Examples of use: 31 | When added to your Sheetworker, the `setInterval()` example above will start working, as will similar uses of `setTimeout()`. These functions now "remember" the character context they were previously loosing, no additional changes need be made to enable these. 32 | 33 | Additionally, this script provides the following "asynchronous safe" versions of the traditional functions used to access attributes: 34 | 35 | ``` 36 | getAttrsAsync() 37 | setAttrsAsync() 38 | getSectionIDsAsync() 39 | ``` 40 | 41 | These three functions return [Promises](https://javascript.info/async) which are especially useful when used with JavaScript's `async/await` syntax. For example: 42 | 43 | ``` 44 | on("sheet:opened", async ()=>{ 45 | var values = await getAttrsAsync(["hp","rec"]); 46 | var newHp=Number(values.hp||0) + Number(values.rec||0); 47 | await setAttrsAsync({hp:newHp}); 48 | values = await getAttrsAsync(["hp"]); 49 | console.assert(values.hp==newHp, "Failed"); 50 | }); 51 | ``` 52 | The details of the above example aren't as important as the fact the function `gets`, `sets`, then `gets` again character attributes without needing to define callbacks. 53 | 54 | ### Part of ORCS 55 | This module is part of the [OnyxRing Client Script](https://github.com/onyxring/ORCS-for-Roll20) collection of scripts for the Roll20 platform. 56 | 57 | It is a standalone script, with no additional dependencies; however, other scripts in the ORCS collection depend on this one. If you are not using the complete version of ORCS, but are instead including portions of it in piecemeal fashion, orcsAsync may already be included by virtue of its inclusion by another member script. 58 | 59 | 60 | Thanks. 61 | 62 | -Jim at OnyxRing 63 | -------------------------------------------------------------------------------- /orcsAsync.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------------- 2 | // orcsAsync 3 | // Compensates for a defect in the underlying Roll20 system, where it "loses context" 4 | // (forgets which player is active) during asynchronous methods and callbacks, 5 | // resulting in error messages like: 6 | // 7 | // Error: Trying to do getAttrs when no character is active in sandbox. 8 | // 9 | // Include this script at the top of your character sheet's code setion to have 10 | // setTimeout() and setInterval() just start working as expected; no additional setup 11 | // required. Additionally, async-safe versions of the typical attribute functions will 12 | // be available: 13 | // 14 | // getAttrsAsync 15 | // setAttrsAsync 16 | // getSectionIDsAsync 17 | // 18 | function isRunningOnServer() { return self.dispatchEvent == undefined; } 19 | function setActiveCharacterId(charId){ 20 | var oldAcid=getActiveCharacterId(); 21 | var msg={"id":"0", "type":"setActiveCharacter", "data":charId}; 22 | 23 | if(isRunningOnServer()==false){ //if in a browser, use "dispatchEvent" to process the message 24 | var ev = new CustomEvent("message"); 25 | ev.data=msg; 26 | self.dispatchEvent(ev); 27 | }else{ //otherwise, use the API (server) message processor, "onmessage" 28 | self.onmessage({data:msg}); 29 | } 30 | return oldAcid; //return what the value used to be, so calling code can be a little cleaner 31 | } 32 | var _sIn=setInterval; 33 | setInterval=function(callback, timeout){ 34 | var acid=getActiveCharacterId(); 35 | _sIn( 36 | function(){ 37 | var prevAcid=setActiveCharacterId(acid); 38 | callback(); 39 | setActiveCharacterId(prevAcid); 40 | } 41 | ,timeout); 42 | } 43 | var _sto=setTimeout 44 | setTimeout=function(callback, timeout){ 45 | var acid=getActiveCharacterId(); 46 | _sto( 47 | function(){ 48 | var prevAcid=setActiveCharacterId(acid); 49 | callback(); 50 | setActiveCharacterId(prevAcid); 51 | } 52 | ,timeout); 53 | } 54 | function getAttrsAsync(props){ 55 | var acid=getActiveCharacterId(); //save the current activeCharacterID in case it has changed when the promise runs 56 | var prevAcid=null; //local variable defined here, because it needs to be shared across the promise callbacks defined below 57 | return new Promise((resolve,reject)=>{ 58 | prevAcid=setActiveCharacterId(acid); //in case the activeCharacterId has changed, restore it to what we were expecting and save the current value to restore later 59 | try{ 60 | getAttrs(props,(values)=>{ resolve(values); }); 61 | } 62 | catch{ reject(); } 63 | }).finally(()=>{ 64 | setActiveCharacterId(prevAcid); //restore activeCharcterId to what it was when the promise first ran 65 | }); 66 | } 67 | //use the same pattern for each of the following... 68 | function setAttrsAsync(propObj, options){ 69 | var acid=getActiveCharacterId(); 70 | var prevAcid=null; 71 | return new Promise((resolve,reject)=>{ 72 | prevAcid=setActiveCharacterId(acid); 73 | try{ 74 | setAttrs(propObj,options,(values)=>{ resolve(values); }); 75 | } 76 | catch{ reject(); } 77 | }).finally(()=>{ 78 | setActiveCharacterId(prevAcid); 79 | }); 80 | } 81 | 82 | function getSectionIDsAsync(sectionName){ 83 | var acid = getActiveCharacterId(); 84 | var prevAcid=null; 85 | return new Promise((resolve,reject)=>{ 86 | prevAcid = setActiveCharacterId(acid); 87 | try{ 88 | getSectionIDs(sectionName,(values)=>{ resolve(values); }); 89 | } 90 | catch{ reject(); } 91 | }).finally(()=>{ 92 | setActiveCharacterId(prevAcid); 93 | }); 94 | } 95 | 96 | -------------------------------------------------------------------------------- /orcsAsync.min.js: -------------------------------------------------------------------------------- 1 | function isRunningOnServer(){return self.dispatchEvent==undefined}function setActiveCharacterId(charId){var oldAcid=getActiveCharacterId();var msg={id:"0",type:"setActiveCharacter",data:charId};if(isRunningOnServer()==false){var ev=new CustomEvent("message");ev.data=msg;self.dispatchEvent(ev)}else{self.onmessage({data:msg})}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();var prevAcid=null;return new Promise((resolve,reject)=>{prevAcid=setActiveCharacterId(acid);try{getAttrs(props,values=>{resolve(values)})}catch{reject()}}).finally(()=>{setActiveCharacterId(prevAcid)})}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)})} --------------------------------------------------------------------------------