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

Announcing orCharacter for Character Sheets (orcsCharacter)

1698100784

Edited 1698535337
Edit: Made some tweaks to the language, and the things it focuses on... orCharacter for character sheets is a self-contained, non-invasive script which significantly simplifies using attributes by implementing JavaScript’s Proxy and Async/Await patterns. It drops easily into new and existing sheets without affecting your existing code and you can mix-and-match it with traditional code as you please. Here’s a short-ish rundown of what orCharacter brings to the table: Attributes as properties The simplest example is reading and writing attributes. Consider the act of healing the player character’s &nbsp;HP&nbsp; with their REC-overy. Using orCharacter, you write this as: pc.HP = await pc.HP + await pc.REC; Compare the above to the following vanilla code which does the same thing: getAttrs(["HP", "REC"], (values) =&gt; { setAttrs({ "HP": Number(values.HP||0) + Number(values.REC||0);} ); }); Of course, this isn’t an overwhelming example, but it&nbsp; does &nbsp;underscore the idea that callbacks, a staple in coding practices of years past, make things more convoluted than their modern&nbsp; async &nbsp;alternatives. Complex tasks can get out of hand quickly. There are a couple of noteworthy points to make about the orCharacter example above: The properties exposing character attributes are asynchronous and leverage the Async/Await features of the JavaScript language. In short, reading and writing properties each interact directly with the Roll20 servers. The&nbsp; await &nbsp;keyword causes code to pause until the server interaction is complete, rather than having to define callback functions. (We make this more efficient in the next section.) orCharacter also attempts to detect and manage numeric attributes. Out of the box, if attributes are “likely” numeric, they are automatically converted to numbers, relieving you from having to write the traditional type-conversion code (e.g.&nbsp; Numeric(attr||0) ). If this behavior doesn’t shake your jive, you can easily turn it off and restore the default Roll20 treatment of attributes by adding this to the top of your SheetWorker: pc.preferNumeric(false); Attributes as cached properties The above “simplest example” follows an “on-demand” philosophy. Read an attribute? Go to the server. Write an attribute? Go to the server. Read the same attribute again? Go to the server. This is fine for many use cases, but for complex or performance-critical code, orCharacter supports bulk reads and writes using&nbsp; cacheAsync() &nbsp;which keeps all changes in memory until we are ready to save them all at once with&nbsp; commitAsyc() : var attrbs = await pc.cacheAsync(["HP", "spiritPool", "efficacy"]); //get several attributes at the same time attrbs.HP = attrbs.HP + attrbs.efficacy; //use and modify them attrbs.spiritPool = attrbs.spiritPool - attrbs.efficacy; attrbs.commitAsync(); //save changes together Notice that, while we may&nbsp; await &nbsp;round trips to the server, we do&nbsp; not &nbsp;do so when reading or writing cached attributes. Also note we aren’t&nbsp; await ing the final&nbsp; commitAsync() &nbsp;call. This is a choice. We would do so if we needed to ensure all attributes were committed in subsequent logic. Simplified Repeating Sections Repeating Sections are awesome, but they also usher in some of The Most Complicated Code in Sheet Workers today. orCharacter makes working with Repeating Sections markedly more intuitive and expressive. Here’s an example which searches the PC’s inventory for all disparate “Bags of Gold” and combines their contents into a single bag: async function combineAllBagsOfGold(){ var totalGold = 0; var inventory = await pc.cacheRepeatingAsync("inventory", ["type", "value"]); for (row of inventory) { if (row.type != "Bag of Gold") continue; totalGold += row.value; row.delete(); } if (totalGold &gt; 0) inventory.addNew({type:"Bag of Gold", value:totalGold}); inventory.commitAsync(); } For comparison, I put together a vanilla flavor of the above. It’s fine. But I want to throw rocks at it and steal its lunch money: function combineAllBagsOfGoldVanilla() { var totalGold = 0; getSectionIDs("repeating_inventory", (ids) =&gt; { var atrbList = ""; ids.forEach((id) =&gt; { if (atrbList != "") atrbList = atrbList + ","; atrbList = atrbList + `"repeating_inventory_${id}_type", "repeating_inventory_${id}_value"`; }); atrbList = JSON.parse("[" + atrbList + "]"); getAttrs(atrbList, (values) =&gt; { ids.forEach(id =&gt; { if (values[`repeating_inventory_${id}_type`] != "Bag of Gold") return; totalGold += Number(values[`repeating_inventory_${id}_value`] || 0); removeRepeatingRow(`repeating_inventory_${id}`); }); if(totalGold&gt;0){ var newId = generateRowID(); setAttrs(JSON.parse(`{"repeating_inventory_${newId}_type" : "Bag of Gold", "repeating_inventory_${newId}_value" : ${totalGold} }`)); } }); }); } While it’s true the orCharacter version is shorter than the traditional version, that isn’t the biggest benefit here. The complexity of the vanilla code, including the nested callbacks and amalgamated strings acting as attribute pointers, makes it harder to grok at a glance. The orCharacter version, by comparison, is easier to follow and therefore support. Tangential benefits I believe a side-effect of improved syntaxes, generally, is they often simplify previously difficult tasks almost accidentally. For example, summing values of a Repeating Section and assigning their total to a character attribute sounds like a trivial undertaking. orCharacter makes this so, not because it provides a specialized&nbsp; addItUp &nbsp;function (because it&nbsp; doesn’t ), but as a consequence of its expressiveness. You can do this… var total = 0, inventory = await pc.cacheRepeatingAsync("inventory", ["value"]); for (item of inventory) total += item.value; pc.HP = total; ...or if you favor conciseness over readability, you can do it in a single expression... pc.HP = (await pc.cacheRepeatingAsync("inventory", ["value"])).reduce((i, v) =&gt; i + v.value, 0); The comparable vanilla code is significantly longer and is expressed as a slightly shorter version of the previous&nbsp; combineAllBagsOfGoldVanilla() &nbsp;example. Installation Installation is a snap. Just paste the content from one of the "complete" files (either "orcsCharacter.complete.js", or the smaller "orcsCharacter.complete.min.js") into the top of your SheetWorker code and you're ready. A PC by any other name... "PC" isn't a very unique name in RPGs and the chance for collision with one of your own variable names is high. It's not a problem. Just copy the content of the&nbsp; pc &nbsp;variable before you assign it in your own code: var _myPlayerCharacter = pc; Or if you'd rather, you can just rename the&nbsp; pc &nbsp;variable to whatever you want by changing the final line of the included script, like so: var _myPlayerCharacter = orcsCharacter.create(); "You are a familiar character, sir." You might have heard of orCharacter before since I once released an even more beta version of it (a “beta-ier” version?), included in a larger API-dependent framework. That whole framework was a lot to bite off for a single, targeted feature. For this discrete release: I've peeled&nbsp; orcsCharacter &nbsp;( this &nbsp;client script flavor of orCharacter) out to stand on its own. It is for Sheet Workers only; there are no API requirements. I’ve refactored the original code to clearly differentiate synchronous from asynchronous functions. If you have been using the version supplied with the beta release of&nbsp; ORAL ORCS , some minor modifications might be required to transition to this revision. The caching/bulk-read-write features described above are brand-spanking new! I’ve included the recently updated&nbsp; Roll20Async &nbsp;script since this is a dependency and JavaScript Promises are a cornerstone of&nbsp; orcsCharacter ’s expressiveness. The enhancements from the previous ORAL ORCS version are non-trivial, so it still gets the beta moniker for now; however, testing so far has also been non-trivial so it should be solid enough to try out in development versions of your character sheets. You can find it here: <a href="https://github.com/onyxring/orcsCharacter" rel="nofollow">https://github.com/onyxring/orcsCharacter</a> &nbsp; If you have any questions, or find bugs, feel free to post these or DM me directly. &nbsp; Thanks, and happy character sheet crafting! &nbsp; Jim (at OnyxRing)
1698115833

Edited 1698115968
GiGs
Pro
Sheet Author
API Scripter
This is an interesting project, but some things make me worry. Take this code: var total = 0, inventory = await pc.getRepeatingAsync("inventory"); for (item of inventory) total += await item.value; pc.HP = total; Is each await item.value a separate call to the server? If so, for complex sheets it doesn't matter that this is neater code. It will be slow enough to be noticeable and cause sheets to lag. I notice you have a cached code option. If the above example is making server calls each time, I think all your code examples should be in cached versions - that should be your default method. I didn't see a cached example for the above code.
I think&nbsp; all&nbsp; your code examples should be in cached versions - that should be your default method. I didn't see a cached example for the above code. Good feedback, G.&nbsp; I updated the example to depict the cached version...&nbsp;
1698247798
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Seconding the performance concerns. Even a basic sheet that only needs say 5 attribute values for a calculation is going to have noticeable lag using a network call for each value. I also think you need a way to combine cacheRepeatingAsync and cacheAsync so that repeating and non repeating attributes can be grabbed with the same network call.
1698264889

Edited 1698265263
Seconding the performance concerns. Even a basic sheet that only needs say 5 attribute values for a calculation is going to have noticeable lag using a network call for each value. Could you clarify the concerns? &nbsp;Certainly reading 5 attributes once is faster than reading 1 attribute 5 times, but that optimization&nbsp; is &nbsp; what the caching approach does. &nbsp;Is the concern that &nbsp; an on-demand version is even an &nbsp; option &nbsp; for the users, or is there something bigger that I’m missing? I also think you need a way to combine cacheRepeatingAsync and cacheAsync so that repeating and non repeating attributes can be grabbed with the same network call. That's an interesting thought.&nbsp; I seem to recall a post of yours...&nbsp; getting is significantly faster that saving ?&nbsp; Obviously there would be&nbsp; some gain, but&nbsp; I’m curious where the point of diminishing returns is.&nbsp; Certainly 2 gets is better than 20, but is the benefit of combining 2 into 1 significant? On side note, related to this, the API version of orCharacter has not yet been updated to support the caching concept.&nbsp; I know that you performed some performance testing on get ting and set ting...&nbsp; were those tests limited to the browser, or did you also gather metrics for API scripts running on the Roll20 servers?&nbsp; Presumably, they'd be faster there, resting on the same backbone as Firebase, but I haven't actually tested this.
1698269450

Edited 1698270680
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
OnyxRing said: Seconding the performance concerns. Even a basic sheet that only needs say 5 attribute values for a calculation is going to have noticeable lag using a network call for each value. Could you clarify the concerns? &nbsp;Certainly reading 5 attributes once is faster than reading 1 attribute 5 times, but that optimization&nbsp; is &nbsp; what the caching approach does. &nbsp;Is the concern that &nbsp; an on-demand version is even an &nbsp; option &nbsp; for the users, or is there something bigger that I’m missing? Yes, my concern is that the non cache option is available. The vast majority of sheet authors are very inexperienced programmers, especially when it comes to JS. It may not occur to most authors just how big a performance impact not using the cache option can have. Additionally, the use cases where the cache option won't be preferable are extremely small. If all of the following are true then using the non-cache method would be acceptable: You're getting less than ~10 attribute values in response to a single event You're setting 2-3 or fewer attribute values in response to a single event But even in these situations, it's only that there's no noticeable performance difference between the cache and non cache methods, not that the non cache method is better. There's also the question of code consistency. Personally I think code is easier to debug/read if it all uses the same architecture. Having the cache and non cache options is encouraging authors to mix and match. While there is no performance issue with that (as long as you are mixing/matching correctly), it makes it very easy to mix up which syntax you're supposed to use. I also think you need a way to combine cacheRepeatingAsync and cacheAsync so that repeating and non repeating attributes can be grabbed with the same network call. That's an interesting thought.&nbsp; I seem to recall a post of yours...&nbsp;&nbsp; getting &nbsp;is&nbsp; significantly faster&nbsp; that&nbsp; saving ?&nbsp; Obviously there would be&nbsp; some&nbsp; gain, but&nbsp; I’m curious where the point of diminishing returns is.&nbsp;&nbsp; Certainly 2&nbsp; gets &nbsp;is better than 20, but is the benefit of combining 2 into 1 significant? It depends on how many gets you need to do. A getAttrs call takes somewhere in the 10s of milliseconds to execute (I think the maximum I ever saw was ~60ms). If we take that upper value, that means that if you need to do 10 gets it's going to take ~500ms, or half a second, just to get all the data from the server. That's long enough for the user to be noticing input lag. That's just for getting, with the way ORCs seems to be setup currently, you'd also be making multiple set calls (even with the cache option) if you need to set repeating attributes and non-repeating attributes at the same time as you need to commit to the section specific object for the repeating attributes and to the generic character for the non repeating. Setting is ~10x as time intensive as getting, which means that in order to hit that "the user notices input lag" mark of ~500ms, you only need about 5 set calls for it to occur. Which brings me to the other thing I just realized that this needs. The notional cacheRepeatingAndBasicAsync needs a way to specify multiple repeating sections (and different attributes for each of those sections). To see why this is needed, let's assume we're updating a reasonably complex sheet (consisting of 20 different repeating sections in addition to the non repeating attributes). Let's further assume that we need to remap attribute values in each of those sections because we're changing how the skill based total modifiers for each section are calculated, and in addition need to recalculate the non repeating skill values themselves. With the current setup of ORCs, I think that would look something like: on('sheet:opened',async ()=&gt;{ // non repeating attribute get const attributes = await pc.cacheAsync(['skill1','skill2','skill3']); // repeating attribute gets const section1 = await package.cacheRepeatingAsync('section1',['attr1','attr2','attr3']); const section2 = await package.cacheRepeatingAsync('section2',['attr1','attr2','attr3']); // ...sections 3 - 19 are the same const section20 = await package.cacheRepeatingAsync('section20',['attr1','attr2','attr3']); // Do whatever logic we're doing with all this data // Commit the non repeating attributes attributes.commitAsync(); // commit the repeating attributes section1.commitAsync(); section2.commitAsync(); // ... sections 3 - 19 committing section20.commitAsync(); }) So, here, we're doing 21 get calls to the server for attribute data. Even at just 10ms for each of those calls, that's still nearly a quarter of a second just to get all of our data. In addition to that, we need to make 21 set calls to the server. While all of those set calls don't need to wait for each other, we still have to specifically commit each repeating section and the non repeating sections separately which makes it very easy to forget to commit one of them and then have a bug that could be very difficult to track down. And depending on when a user last opened the sheet or played this game, they might have multiple of these update functions to run through with each one requiring at minimum noticeable fractions of a second to execute. That's quickly going to escalate to the point where just booting up a sheet after a few months of updates without accessing it becomes prohibitive. Now, to be fair, with async/await unlocked we can speed that up some by for instance waiting on a Promise.all() of each of these instead: const [ attributes, section1, section2, // ...sections 3 - 19 are the same section20 ] = await Promise.all([ // non repeating attribute get pc.cacheAsync(['skill1','skill2','skill3']), // repeating attribute gets pc.cacheRepeatingAsync('section1',['attr1','attr2','attr3']), pc.cacheRepeatingAsync('section2',['attr1','attr2','attr3']), // ...sections 3 - 19 are the same pc.cacheRepeatingAsync('section20',['attr1','attr2','attr3']) ]); But that adds complexity to our code when the goal is less complex code. On side note, related to this, the API version of orCharacter has not yet been updated to support the caching concept.&nbsp; I know that you performed some performance testing on&nbsp; get ting and&nbsp; set ting...&nbsp; were those tests limited to the browser, or did you also gather metrics for API scripts running on the Roll20 servers?&nbsp; Presumably, they'd be faster there, resting on the same backbone as Firebase, but I haven't actually tested this. I just tested the speed of the sheet based setAttrs/getAttrs. The API gets/sets are definitely faster though since they are actually synchronous. However, you'd still need to account for passing the various messages back and forth to the database to then trigger the API. A note on the resonableness of this example This may seem like an unlikely number of repeating sections to have in a live sheet, however I'd point out that the&nbsp; Roll20 5e sheet has 36 different repeating sections in it spread across the PC, NPC, Vehicle, and Kingdom displays. The Pathfinder community sheet also has a number of repeating sections in the high teens at the least. Pretty much any sheet beyond the most basic is going to have a large number of these sections that might all need to be worked on or have values gotten from them at once. EDIT: I also noticed another problem with the non cache method that hadn't occurred to me earlier. Let's assume that you are calculating several different attributes, some of which depend on other attributes that are being recalculated in this same event. With the non cache method, I think that would look something like: on('change:base_1 change:base_2',async ()=&gt;{ // Attribute setting that is just reliant on values straight from the sheet pc.dependent_1 = await pc.base_1 + 10; // setting an attribute that is reliant on one of the attributes we're setting in this event listener pc.sub_dependent_1 = await pc.dependent_1 + await pc.base_2; }) Because of the asynchronous nature of the setAttrs that happens in the background, and because getting is faster than setting, it's very likely that the value you'd be getting for dependent_1 &nbsp;would be the old value, not the value that was just set. The obvious workaround for this is to store your intermediate values in basic JS variables instead of setting directly to the sheet attribute, but that is again adding code complexity and getting rid of the big benefit of using the non cache method.
1698272452
GiGs
Pro
Sheet Author
API Scripter
Those are all good points. I do think All sets in a single worker should be bundled together As Scott suggests, there is no need to have two different versions of code that does the same thing. The cached version should be presented as the single method. Working with multiple repeating sets is incredibly common, and needs to be handled. Calling them repeating sets is accurate because they multiply like crazy.
OnyxRing asked: Is the concern that &nbsp; an on-demand version is even an &nbsp; option &nbsp; for the users, or is there something bigger that I’m missing? Scott Said: Yes, my concern is that the non cache option is available. I understand.&nbsp; That's a question of design philosophy, which I welcome the discussion of, but also feel we can safely fall on different sides of.&nbsp; GiGs's recommendation of training the users seems like a prudent approach to leaning newbies toward the preferred way, but leaving the on-demand option intact for the edge cases where it might make more sense: GiGs said: I think &nbsp; all&nbsp; your code examples should be in cached versions - that should be your default method.&nbsp; Your example of 20 Repeating Sections gives me pause.&nbsp; I hadn't considered that it might be common to read that many attributes from that many repeating sections all at once; but it intrigues me, especially since I suspect writing such a retrieval in traditional code would be a nightmare and I'd like to see it solved more elegantly.&nbsp; &nbsp; Does something like this syntax resonate? var repSections = await pc.cachMultipleRepeatingAsync( { "armor":["def", "weight", "power"], "heldItems":["defAdj", "weight"], "enchantments":["defAdj", "cost"], ...}); &nbsp;The returned repSections would be a collection of the same cached Repeating Section objects returned by cachRepeatingAsync(), which you could iterate over&nbsp; with .forEach(...), or access the individual sections by name (e.g., repSections.armor or repSections["armor"] ). In this world, committing individual Repeating Sections is one option (repSections.armor.commitAsync()), but so is committing all changes to all Repeating Sections in one big push: repSections.commitAsync(); This wouldn't be too difficult; most of the plumbing to track changes and merge these between objects is already in place.&nbsp; What the approach doesn't handle are core attributes, which I guess could be implemented with something kludgy, like: repSections.commitWithCachedAttributes(attribs); But again, I wonder if the 1 set vs 2 sets&nbsp; is worth the extra syntax. We're pretty far down the rabbit hole at this point; I feel like we're chasing some uncommon edge cases, but definitely correct me if I'm wrong. Thanks for letting me pick your brain! - Jim
1698281228
GiGs
Pro
Sheet Author
API Scripter
OnyxRing said: OnyxRing asked: Your example of 20 Repeating Sections gives me pause.&nbsp; I hadn't considered that it might be common to read that many attributes from that many repeating sections all at once; but it intrigues me, especially since I suspect writing such a retrieval in traditional code would be a nightmare and I'd like to see it solved more elegantly.&nbsp; &nbsp; Does something like this syntax resonate? var repSections = await pc.cachMultipleRepeatingAsync( { "armor":["def", "weight", "power"], "heldItems":["defAdj", "weight"], "enchantments":["defAdj", "cost"], ...}); &nbsp;The returned repSections would be a collection of the same cached Repeating Section objects returned by cachRepeatingAsync(), which you could iterate over&nbsp; with .forEach(...), or access the individual sections by name (e.g., repSections.armor or repSections["armor"] ). In this world, committing individual Repeating Sections is one option (repSections.armor.commitAsync()), but so is committing all changes to all Repeating Sections in one big push: repSections.commitAsync(); This wouldn't be too difficult; most of the plumbing to track changes and merge these between objects is already in place.&nbsp; What the approach doesn't handle are core attributes, which I guess could be implemented with something kludgy, like: repSections.commitWithCachedAttributes(attribs); But again, I wonder if the 1 set vs 2 sets&nbsp; is worth the extra syntax. We're pretty far down the rabbit hole at this point; I feel like we're chasing some uncommon edge cases, but definitely correct me if I'm wrong. Thanks for letting me pick your brain! - Jim IIRC, I didn't say 20 repeating sections, but you can get a lot. And you might need to access all of them with a version update worker. The syntax you describe for multiple repeating sections does resonate, because I used the same syntax for my own "multiple repeating sections" function - though it does have the drawback of needing a separate getSectionIDs call for each repeating section (just one for each) - see <a href="https://cybersphere.me/multiple-repeating-sections-custom-function/" rel="nofollow">https://cybersphere.me/multiple-repeating-sections-custom-function/</a> The ideal approach is probably to have one massive getAttrs at the start of a worker, where you call all attributes just once (normal and repeating sections included), and one setAttrs, where you save all all values (and maybe only those that have changed - you can compare current values with the original values to see which have changed). This would be a lot like your cached system with addition structure for the setAttrs part. In theory you could have just one worker - a massive structure that makes all changes your sheet needs. I personally don't go that route for the complexity it creates, but I have come close a few times. This method does have promise: repSections.commitWithCachedAttributes(attribs);
IIRC, I didn't say 20 repeating sections I actually didn't realize you had responded before me, and I was referring to Scott's theoretical example above. &nbsp;see&nbsp; <a href="https://cybersphere.me/multiple-repeating-sections-custom-function/" rel="nofollow">https://cybersphere.me/multiple-repeating-sections-custom-function/</a> So this is good!&nbsp; I enjoyed that article quite a bit and find it particularly clever.&nbsp; Had I known it existed, it almost certainly would have impacted the orCharacter implementation. The ideal approach is probably to have one massive getAttrs at the start of a worker.. ... and one setAttrs, where you save all all values. Uff!&nbsp; I did toy with this concept of getting it all up front, then saving changes at the end, or on a timer which I believe you suggested in an older thread&nbsp; (orCharacter absolutely does track changes and only commits those attributes which have changed, BTW).&nbsp; It's still possible to implement this&nbsp; using the library, but I didn't build it&nbsp; into the orCharacter.&nbsp; I shy away from this approach primarily because attributes can change outside of the SheetWorker, from the API, invalidating your cache. This is probably the complexity you refer to. The syntax you describe for multiple repeating sections does resonate ... This method does have promise: repSections.commitWithCachedAttributes(attribs); Okay.&nbsp; This has been a productive exchange and I'm reluctantly starting to come around to minimizing getRepeatingAsyc.&nbsp; Not removing it from the library, mind you, I actually have a use case where I think it will be more natural use, but certainly shining the spotlight on cacheRepeatingAsync.&nbsp; I'll spend a little time piecing together a working version of the "Get/Set-Everything-Everywhere-All-At-Once" additions to the library which are outlined above.
1698285909
GiGs
Pro
Sheet Author
API Scripter
OnyxRing said: Okay.&nbsp; This has been a productive exchange and I'm reluctantly starting to come around to minimizing getRepeatingAsyc.&nbsp; I do think the cache version should be the norm, but I agree there's no need to eliminate this entirely. I was visualising it more as a sidebar, demonstrating how the code does what it does and presented as an option. But I do think it should be relgated to an just an option, and not given prominence. Thank you about the positive comments :)
1698285948
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
The ideal approach is probably to have one massive getAttrs at the start of a worker, where you call all attributes just once (normal and repeating sections included), and one setAttrs, where you save all all values (and maybe only those that have changed - you can compare current values with the original values to see which have changed). This would be a lot like your cached system with addition structure for the setAttrs part. Heh, I went there for you GiGs. This is how K-scaffold operates. Grabs everything you might possibly need from the sheet, and provides it to any function called by the scaffold's cascade. While the actual scaffold is complex as hell, the code you write to use it is pretty simple. Your example of 20 Repeating Sections gives me pause.&nbsp; I hadn't considered that it might be common to read that many attributes from that many repeating sections all at once; but it intrigues me, especially since&nbsp; I suspect writing such a retrieval in traditional code would be a nightmare &nbsp;and I'd like to see it solved more elegantly.&nbsp; &nbsp; Does something like this syntax resonate? The 20 repeating sections isn't something that's going to be in every function, but personally I have to use something that far reaching often enough that it's always in the back of my mind. For the syntax, I might propose a small change so that you could do one declaration and get both repeating and non repating attributes: const sheet = await pc.cacheAll([ {attributes:['non_repeating_1','non_repeating_2']}, {section:'armor',attributes:['armor_value','name']}, {section:'weapons',attributes:['weapon_damage','accuracy']} ]); This would then get everything you need for a given event, repeating and non repeating.
1698286049

Edited 1698561960
on('change:base_1 change:base_2',async ()=&gt;{ // Attribute setting that is just reliant on values straight from the sheet pc.dependent_1 = await pc.base_1 + 10; // setting an attribute that is reliant on one of the attributes we're setting in this event listener pc.sub_dependent_1 = await pc.dependent_1 + await pc.base_2; }) Because of the asynchronous nature of the setAttrs that happens in the background, and because getting is faster than setting, it's very likely that the value you'd be getting for&nbsp; dependent_1 &nbsp;would be the old value, not the value that was just set.&nbsp; Right!&nbsp; So if you needed the value to be committed for subsequent use, then this would be the correct version of the first line of code within the callback (edited after original reply): await ( pc.dependent_1 = await pc.base_1 + 10);
1698286111
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: The ideal approach is probably to have one massive getAttrs at the start of a worker, where you call all attributes just once (normal and repeating sections included), and one setAttrs, where you save all all values (and maybe only those that have changed - you can compare current values with the original values to see which have changed). This would be a lot like your cached system with addition structure for the setAttrs part. Heh, I went there for you GiGs. This is how K-scaffold operates. I know :) I only thought of this approach because of your previous comments on the topic.
1698286359

Edited 1698286519
For the syntax, I might propose a small change so that you could do one declaration and get both repeating and non repating attributes: const sheet = await pc.cacheAll([ {attributes:['non_repeating_1','non_repeating_2']}, {section:'armor',attributes:['armor_value','name']}, {section:'weapons',attributes:['weapon_damage','accuracy']} ]); This would then get everything you need for a given event, repeating and non repeating. Let me mule that over for a while.&nbsp; It may be the right way to go.
Okay.&nbsp; I have these updates committed.&nbsp; The final syntax I landed on is this: var sheet = await pc.cacheMultipleAsync(["HP", "REC"], //basic attribs { &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; inventory: ["name","type", "value"], &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//repeating sections &nbsp;&nbsp;&nbsp;&nbsp; enchantments:["name","power", "effect"] }); The attributes across all objects are pulled in with a single getAttrs.&nbsp; The basic attributes are exposed on a property called "attributes";&nbsp; the repeating sections are exposed in an array off a property called "sections".&nbsp; These are the same objects returned from cacheAsync and cacheRepeatingAsync and can be used in exactly the same way: sheet.attributes.HP = sheet.attributes.HP + sheet.attributes.REC; sheet.sections.inventory[1].type="Bag of Gold"; When saved, all modifications are rolled together and sent to the server in a single commit: sheet.commitAsync(); This addition passes my test cases, both my new and my regression ones, but it wouldn't surprise me to find some new bugs crop up, both new and regression ones.