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] SheetWorker setAttrs silently

1636262088
Finderski
Pro
Sheet Author
Compendium Curator
I'm having a problem with a sheet worker I want to set values silent, but it seems to keep triggering other sheet workers (i.e. not setting things silently). Here's the full sheet worker code: const swapAttribute = function ( section , oldField , newField , silent ) { //oldField and newField can be strings of single fields or arrays of multiple fields //silent should be a boolean true/false and indicates whether the setAttrs should be set silently log ( " Section " , section , " orange " ); log ( " oldField " , oldField , " orange " ); log ( " newField " , newField , " orange " ); let repeatingSection = ` repeating_ ${ section } ` ; let output = {}; let fieldList = []; let sectionList = []; let newFieldList = []; //build list of fields to get the value of the field getSectionIDs ( repeatingSection , function ( idArray ) { log ( ` Number of Rows in ${ repeatingSection } ` , idArray . length , " orange " ); if ( idArray . length > 0 ) { log ( " idArray " , " Not Empty, made it to the If Statement " , " orange " ); for ( let a = 0 ; a < idArray . length ; a ++ ) { log ( " Loop Count " , a , " orange " ); let fullField ; if ( typeof oldField === " string " ) { fullField = ` ${ repeatingSection } _ ${ idArray [ a ] } _ ${ oldField } ` ; fieldList . push ( fullField ); sectionList . push ( idArray [ a ]); } else { for ( let b = 0 ; b < oldField . length ; b ++ ) { fullField = ` ${ repeatingSection } _ ${ idArray [ a ] } _ ${ oldField [ b ] } ` ; fieldList . push ( fullField ); newFieldList . push ( ` ${ repeatingSection } _ ${ idArray [ a ] } _ ${ newField [ b ] } ` ); sectionList . push ( idArray [ a ]); } } } } else { log ( ` ${ repeatingSection } empty ` , " Nothing to see here...move along. " , " orange " ); } //get the values of the "old" field log ( " Field List " , fieldList , " orange " ); getAttrs ( fieldList , function ( v ) { for ( let b = 0 ; b < fieldList . length ; b ++ ) { log ( " getAttrs Loop " , b , " orange " ); let fieldValue = v [ fieldList [ b ]]; log ( " Field Value " , fieldValue , " orange " ); log ( " Value Type " , typeof fieldValue , " orange " ); typeof fieldValue === " undefined " ? fieldValue = " " : fieldValue = fieldValue ; output [ ` ${ newFieldList [ b ] } ` ] = fieldValue ; } log ( " Output " , JSON . stringify ( output ), " orange " ); if ( silent ) { log ( " Set Silently " , " Setting the values silently " , " orange " ); setAttrs ( output ,{ " silent " : true }); } else { log ( " Set Silently " , " NOT Setting the values silently " , " orange " ); setAttrs ( output ); } }); }); }; And here's a snapshot of the dev console for this... You'll see the triggering event is a sheetworker for the same fields that were just set.   All the documentation I can find (and even in the forums) indicates that the correct way to set values silent is this format: setAttrs(output,{"silent":true}); I've tried silent with and without the double quotes. True is never quoted, because I need it to be the boolean.  The variable output (as noted above in the code) is a JSON object with the key/value pairs that need to be set. So, what am I doing wrong? I can't seem to spot it. :(
1636301269

Edited 1636303219
GiGs
Pro
Sheet Author
API Scripter
I don't know what's going on here, probably would need to see the full workflow. But you do have a pretty convoluted structure there. What are you using to detect that changes have occurred? Is it detecting just a partial set of the fields as changed, or the entire output? (In otehrwords, is silent working for some attributes but not others, or is it failing for all of them?) By the way, I'm not sure it's a good idea to have a function that has a setAttrs inside it. That easily leads to multiple cascading setAttrs calls when you have multiple calls to that function. It's better to have your function return an object which contains the stats and their values you want to set. Then you can handle the setAttrs in the originating worker - and most importantly, can combine different function calls if needed. I'm only mentioning this because I have seen many sheets do this, with often leads to many many cascading setAttrs calls within the same worker. You might not be doing that, but I thought it worth mentining just in case.
1636302466

Edited 1636302491
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I think GiGs' suggestion of possible multiple setAttrs is a very good suggestion here. There's been several reports that firing multiple setAttrs at once disables the {silent} flag. Something else to look at is what your listener that triggers off of these attributes looks like. There's some funkiness that happens with sheetworkers setting repeating attributes if the listener is using the generic attribute listener construction for a repeating attribute (e.g. on('change:attribute_name')  instead of on('change:repeating_section:attribute_name') .
1636315699

Edited 1636315821
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: I think GiGs' suggestion of possible multiple setAttrs is a very good suggestion here. I thought you'd like that :) I've been looking closer at your function, Finderski. i notice yiu create a sectionList array in getSectionIDs, but it's never used later in the function. Is it important?
1636318598
Finderski
Pro
Sheet Author
Compendium Curator
Thanks for the replies.  I'll address the easies questions first, then get with the updates and harder questions... GiGs said: I've been looking closer at your function, Finderski. i notice yiu create a sectionList array in getSectionIDs, but it's never used later in the function. Is it important? It was used in an earlier version of this function; I've made changes and just haven't gotten around to removing that, yet, because I may need it.  I'll comment it out for now. Scott C. said: There's some funkiness that happens with sheetworkers setting repeating attributes if the listener is using the generic attribute listener construction for a repeating attribute (e.g. on('change:attribute_name')  instead of on('change:repeating_section:attribute_name') . The on('change...) is listening for the full change:repeating_skills:attribute_name  format. GiGs said: I don't know what's going on here, probably would need to see the full workflow. But you do have a pretty convoluted structure there. Not sure how to make it less convoluted...I actually thought this was pretty straightforward (at least compared to some of my other stuff...) :-/ In summary, the work flow is this: On sheet opened check version of the sheet > if sheet version is lower than x (in this case 0.18) then... if ( versionthree < 0.18 ) { //Get Old Knowledge Skills let setAttrsObj = swapAttribute ( " skills " , [ " skillnamerank_base " , " skillnamerankmod_base " , " skillnamerank_delta " , " skillnamerank " , " skillnamerankmod " , " otherModType " , " SkillMod " , " linkedotherattscore " , " whisperskillname " , " skillnamewilddie " , " skillnamewilddieinfo " , " skillnamerank_rank " , " skillnamerank_display " ], [ " knowledgeother_base " , " knowledgeothermod_base " , " knowledgeother_delta " , " knowledgeother " , " knowledgeothermod " , " knowledgeotherModType " , " KnowledgeotherskillMod " , " linkedknowledgeotheratt " , " whisperknowledgeother " , " knowledgeotherwilddie " , " knowledgeotherwilddieinfo " , " knowledgeother_rank " , " knowledgeother_display " ], true ); log ( " setAttrsObj " , JSON . stringify ( setAttrsObj ), " orange " ); // //Fix Weapon Skill Rolls //stepwiseFaux("weapons","weaponskill"); } GiGs said: What are you using to detect that changes have occurred? Is it detecting just a partial set of the fields as changed, or the entire output? (In otehrwords, is silent working for some attributes but not others, or is it failing for all of them?) Here's the on change that's getting triggered: //Create Trait values for rank and display hidden fields traits . forEach (( trait ) => { on ( ` change: ${ trait } change: ${ trait } mod ` , function ( eventInfo ) { /*change:${trait}_delta*/ log ( " Change Detected " + " (source type: " + eventInfo . sourceType + " ) " , ` for ${ trait } ` , " cyan " ); log ( " Trait triggering update " , eventInfo . sourceAttribute , " cyan " ); //let trait = traits; //console.log("trait = " + trait); log ( " Check for repeating " , trait . slice ( 0 , 6 ), " cyan " ); /*if (trait.slice(0,5) === 'repeating') { traitcheck = `repeating_skills_${traits}`; }*/ trait = trait . replace ( " : " , " _ " ); //console.log("trait = " + trait); getAttrs ([ trait , ` ${ trait } mod ` , ` ${ trait } _delta ` ], ( v ) => { log ( " Get Attrs " , ` ${ trait } , ${ trait } mod, ${ trait } _delta ` , " cyan " ); log ( " intMod " , v [ ` ${ trait } mod ` ], " cyan " ); log ( " Delta " , v [ ` ${ trait } _delta ` ], " cyan " ); let tDelta = v [ ` ${ trait } _delta ` ]; tDelta = parseInt ( tDelta ) || 0 ; let tDie = parseInt ( v [ trait ]) || 0 ; log ( " Values " , ` ^^^ Delta: ${ tDelta } , tDie: ${ tDie } ^^^ ` , " cyan " ); let tRank = " 1d " ; // let tDisplay = (tDelta === 0 ? "d" : "*d"); let tDisplay = tDelta === 0 ? getTranslationByKey ( " dice-abbreviation " ) : " * " + getTranslationByKey ( " dice-abbreviation " ); let intMod = v [ ` ${ trait } mod ` ]; intMod = parseInt ( intMod ) || 0 ; tRank += v [ trait ] + " ! " ; tDisplay += v [ trait ]; if ( intMod > 0 ) { log ( " Message " , " Mod is > 0...use + " , " cyan " ); tRank += " + " + intMod + " [trait] " ; tDisplay += " + " + intMod ; } else if ( intMod < 0 ) { log ( " Message " , " Mod is < 0...use - " , " cyan " ); tRank += intMod + " [trait] " ; tDisplay += intMod ; } else { log ( " Message " , " Mod is 0...don't do anything " , " cyan " ); } const sAttRank = trait + " _rank " , sAttDisplay = trait + " _display " ; const sattrs = {}; sattrs [ sAttRank ] = tRank ; sattrs [ sAttDisplay ] = tDisplay ; setAttrs ( sattrs ); }); }); }); The important part here, is traits is a list of skill names that need to be watched for a change, so it can generate to values that are needed for the sheet to function.  Within that array of strings is: " repeating_skills:knowledgeother " , That is the part that's being triggered...or was...I'll explain the new development in a minute... GiGs said: By the way, I'm not sure it's a good idea to have a function that has a setAttrs inside it. That easily leads to multiple cascading setAttrs calls when you have multiple calls to that function. It's better to have your function return an object which contains the stats and their values you want to set. Then you can handle the setAttrs in the originating worker - and most importantly, can combine different function calls if needed. I'm only mentioning this because I have seen many sheets do this, with often leads to many many cascading setAttrs calls within the same worker. You might not be doing that, but I thought it worth mentining just in case. Now we get to the new development and a couple of other questions... I updated my code to get that setAttrs out of the function; now that entire getAttrs bit looks like this: getAttrs ( fieldList , function ( v ) { for ( let b = 0 ; b < fieldList . length ; b ++ ) { log ( " getAttrs Loop " , b , " orange " ); let fieldValue = v [ fieldList [ b ]]; log ( " Field Value " , fieldValue , " orange " ); log ( " Value Type " , typeof fieldValue , " orange " ); typeof fieldValue === " undefined " ? fieldValue = " " : fieldValue = fieldValue ; /*output[ `${repeatingSection}_${sectionList[b]}_${newField}` ] = fieldValue;*/ outputOBj [ ` ${ newFieldList [ b ] } ` ] = fieldValue ; } log ( " Output " , JSON . stringify ( outputOBj ), " orange " ); return outputOBj ; }); The code update to the code that calls that function is: if ( versionthree < 0.18 ) { //Get Old Knowledge Skills let setAttrsObj = swapAttribute ( " skills " , [ " skillnamerank_base " , " skillnamerankmod_base " , " skillnamerank_delta " , " skillnamerank " , " skillnamerankmod " , " otherModType " , " SkillMod " , " linkedotherattscore " , " whisperskillname " , " skillnamewilddie " , " skillnamewilddieinfo " , " skillnamerank_rank " , " skillnamerank_display " ], [ " knowledgeother_base " , " knowledgeothermod_base " , " knowledgeother_delta " , " knowledgeother " , " knowledgeothermod " , " knowledgeotherModType " , " KnowledgeotherskillMod " , " linkedknowledgeotheratt " , " whisperknowledgeother " , " knowledgeotherwilddie " , " knowledgeotherwilddieinfo " , " knowledgeother_rank " , " knowledgeother_display " ], true ); log ( " setAttrsObj " , JSON . stringify ( setAttrsObj ), " orange " ); // //Fix Weapon Skill Rolls //stepwiseFaux("weapons","weaponskill"); } else { log ( " Upgrade " , ` Sheet already updgraded ( ${ characterName } ) ` , " lime " ); } //setAttrs(setattrvals); setAttrs ( setAttrsObj ,{ " silent " : true }); And now, nothing gets set... that last setAttrs never fires. I don't know why, or what I'm doing wrong now...? I think I'm returning the object properly, once I call that function, it never returns... 1. Probably, because it's inside a getAttrs? Assuming so, how do I get the object data I need out of that the getAttrs so it can return it to the calling section/function so I can have a single setAttrs? 2. What do you do when you need to have two different setAttrs calls? One that's silent and one that's not? In this particular workflow there are some other things happening on sheet open that need to be done, but none of them trigger nor watch the field that's getting triggered by the silent setAttrs (or rather the setAttrs that's failing it's Stealth roll).
1636333512

Edited 1636333793
Oosh
Sheet Author
For the first question - you're correct. getAttrs is returning a value to the outer function (assuming it's still swapAttribute, it's not in the latest code chunk), but swapAttributes has probably already finished executing by the time getAttrs returns a value, because getAttrs is asynchronous. I'm a big fan of using Promisified sheetworkers and the async/await pattern (and so is Scott), and so I'm a bit rusty on callbacks, but here is a simple example: // Standard function returning a value at the end, with an async function inside const outerFunction = ( input ) => {     let output = {};     setTimeout (() => {         output [ input ] = true ;     }, 50 );     return output ; } // Define a callback function as a parameter, and return the required value from the inner function const outerFunctionWithCB = ( input , callback = ( v ) => { return v }) => {     let output = {};     setTimeout (() => {         output [ input ] = true ;         callback ( output );     }, 50 );     // return output; } console . log ( outerFunction ( 'blah' )); // Expected output: {} outerFunctionWithCB ( 'blah' , callbackVal => console . log ( callbackVal )); // Expected output: {blah: true} The first one shows what (I'm guessing) your swapAttributes currently does, with the setTimeout pretending to be a getAttrs call. The order outerFunction() gets processed in is - let output={} - setTimeout begins, but is then moved to the bottom of the stack as it is asynchronous - return output: {} - setTimeout sets a property on the output object ~50ms later , output is now {blah: true} The second function fixes this the old-fashioned way, with a callback. You then call the outer function in the same way as getAttrs - input parameters first, followed by a callback function with the code for dealing with the output. So you can add a super simple callback return to your outer function (with standard function declaration instead of fat arrows... probably should've noticed that before I started using them for the example....) const swapAttribute = function ( section , oldField , newField , silent , result = function ( v ) { v }) { Then return your output object to the callback at the end of the getAttrs (by the way, is the 'B' supposed to be capitalized in OBj?):     log ( "Output" , JSON . stringify ( outputOBj ), "orange" );     result ( outputOBj ); And call the setAttrs inside the function's callback: if ( versionthree < 0.18 ) {     //Get Old Knowledge Skills     swapAttribute ( "skills" , [ /* oldField */ ], [ /* newField */ ], function ( setAttrsObj ) {         log ( "setAttrsObj" , JSON . stringify ( setAttrsObj ), "orange" );         //         //Fix Weapon Skill Rolls         //stepwiseFaux("weapons","weaponskill");         //setAttrs(setattrvals);         setAttrs ( setAttrsObj ,{ "silent" : true });     }); } else {     log (         "Upgrade" ,         `Sheet already updgraded ( ${ characterName } )` ,         "lime"     ); }
1636336393

Edited 1636347163
GiGs
Pro
Sheet Author
API Scripter
This reveals a couple of things that change my advice: First, with a version change it's less important to be efficient in terms of setAttrs - it's still good to be, but it doesnt matter that much if you have a bit of extra lag when something is going to only run once per character. Second, you are only running this function once, not multiple times - I suggested moving setAttrs out for the latter possibility. Edit: Oosh has already addressed the start of the next paragraph. I got carried away with other stuff... :) For your questions, its not running because you need to run the setAttrs inside the getAttrs that calls it, so you probably need to construct that function call with a callback.Considering the two points above, it's easier to just write the function kind of the way you originally had it (but I'd replace the for statements with forEach or for more advanced work, reduce - that way you can avoid the combined if and for loops. A forEach only runs if you have 1 or more items-  you don't need to check for zero. And you can get an index of the forEach with an optional parameter, which lets you do something like this (written quickly): getAttrs ( oldFieldList , function ( v ) {     // can loop through oldFieldList, but traditionally I prefer to loop through idArray again.     fieldList . forEach (( oldField , fieldIndex ) => {         let fieldValue = v [ oldField ];         typeof fieldValue === "undefined" ? fieldValue = " " : fieldValue = fieldValue ;         output [ ` ${ newFieldList [ fieldIndex ] } ` ] = fieldValue ;     });     setAttrs ( output ); }); Notice the extra index (fieldIndex) parameter in forEach, which you can then use to grab the second field address. Using forEach like this in your getSectionIDs section can make that section a lot less clunky (with its ifs and fors). If you have two different setAttrs calls, one silent and one not, that's a case where you'd build separate attribute lists and run setAttrs twice. But remember, this would could be multiple swap function calls, each returning attribute sets, which are then processed and combined, and you then run setAttrs once or twice - instead of once per swap function which could be many more times than twice. But as I said above, since this is a version function and wont be run that many times, its probanly not something to worry about. Final suggestion: in your getSectionIDs, you have a test for if the oldField is a string, then you run a different process whether its a string or an array. What I would do differently, is check if its a strange, and if so, convert it to an array (of length 1). Then have the same procedure run afterwards. So something like this: if ( typeof oldField === "string" ) {     oldField = [ oldField ];     newField = [ newField ]; } // you might want to include some checking that oldfield and newField are indeed the same type, and the same length. Then since you know they are both arrays, you can use the same procedure to construct the fieldList and newFieldList. My first thought was using forEach again like this: const repeatingField = ( section , id , field ) => ` ${ section } _ ${ id } _ ${ field } ` ; idArray . forEach ( id => {     oldField . forEach (( field , fieldIndex ) => {         // we don't need to use index here, because forEach gives is the value directly.         fieldList . push ( repeatingField ( repeatingSection , id , field ));         // we do need to use the index here, because we are accessing a different array, and want the same index there.         newFieldList . push ( repeatingField ( repeatingSection , id , newField [ fieldIndex ]));     }); }); Notice the function on the first line to build repeating section names. But that still seemed a bit convoluted, then I remembered map . The map function always returns an array, so you can do this: const repeatingField = ( section , id , field ) => ` ${ section } _ ${ id } _ ${ field } ` ; idArray . forEach ( id => {     fieldList = [... fieldList , ...oldField . map ( field => repeatingField ( repeatingSection , id , field ))];     newFieldList = [... newFieldList , ...newField . map ( field => repeatingField ( repeatingSection , id , field ))]; }); Um, I guess this depends on your definition of convoluted. Your original code is more straightforward if you dont know these structures. So in this: fieldList = [...fieldList, ...anArray]; the ... is the spread operator, which expands both those arrays into their elements. And the outer [ ] puts them back into an array. So its a way to combine multiple arrays. The map function creates an array of the repeating section names from the oldField and newField lists. You might need some error handling for what happens if oldField and newField actually have 0 entries, but you probably should test for that earlier and exit the function - because the rest of the code assumes you have one or more entries. Caveat - this is all spitballing, and I havent tested any of this code.
1636356989
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Oosh said: I'm a big fan of using Promisified sheetworkers and the async/await pattern (and so is Scott), and so I'm a bit rusty on callbacks, but here is a simple example... I was, and I'd still love to see it become officially supported. Unfortunately, the async/await stuff breaks the API sandbox so that API scripts can't trigger sheetworkers. :(
1636383942
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: Oosh said: I'm a big fan of using Promisified sheetworkers and the async/await pattern (and so is Scott), and so I'm a bit rusty on callbacks, but here is a simple example... I was, and I'd still love to see it become officially supported. Unfortunately, the async/await stuff breaks the API sandbox so that API scripts can't trigger sheetworkers. :( Yeah, I'd love to use those functions too, but have avoided them because they aren't officially supported.
1636384439
Finderski
Pro
Sheet Author
Compendium Curator
Thanks for the help, y'all...it sounds like these suggestion will make things more efficient, but won't fix the issue of setting things silently work, correct?  I'll work on cleaning things up, though... I wonder if the jQuery stuff has broken other parts of sheet workers, though? I'm finding that my getAttrs values are showing as undefined if the field it's calling is empty or only has a default value; I can't seem to set things with empty strings either...it's really getting frustrating trying to work with sheet workers when my coding skills are already inadequate, and then having to figure out how to work around these other things without breaking buttons (for example, if the field on the sheet has a default value of 0 in it, and it comes into the sheet worker as undefined I can't set it as " " because that'll break any rolls relying on that value, but unless I add a lot of overhead to manually check which field is being set, then I have to set all undefined values to 0 and hope that value is correct, and still get 0's in text fields that may be part of the same worker).
1636385561

Edited 1636385597
GiGs
Pro
Sheet Author
API Scripter
I asked earlier if one attribute in the set was being unsilent, or if the whole set of output was. Scott mentioned that multiple setAttrs in a row could be causing the issue. There have been no follow-ups to those, so it's hard to guess. It does sound though that a lot of things are not working properly, so it may be that something else broken. I did wonder why in your code you were checking for undefined then setting it as " ". Ideally you want to set it as either a 0 or a "" depending on the input type. That's not a problem I've experienced, because I always know where I should be sending a 0 or a "", or just cant get undefined values (I need to do some newer checking to see if this has changed). So one problem may be that you are trying to be too general.
1636385820

Edited 1636385840
GiGs
Pro
Sheet Author
API Scripter
I just remembered something I meant t oask: You are setting a repeating section here. Does it actually matter that its not being sent as silent? Does your repeating section have attributes that can cause changes elsewhere in the sheet? In my experience (which admittedly may be limited), it's unusual for repeating sections to do this - if only because its harder to code for. I guess there could be changes that casuse changes in the same row of the repeating section, but that again raises the question: does it actually matter if the change event fires? The attributes will be set (reset) to values you actually want them set to, and its a version change worker so it'll only happen once, so lag wont be a real issue.
1636386365
Finderski
Pro
Sheet Author
Compendium Curator
GiGs said: I asked earlier if one attribute in the set was being unsilent, or if the whole set of output was. I'm not sure how to answer that question...the important pieces I need set silently aren't being set silently, I know that. GiGs said: I just remembered something I meant t oask: You are setting a repeating section here. Does it actually matter that its not being sent as silent? Does your repeating section have attributes that can cause changes elsewhere in the sheet? In my experience (which admittedly may be limited), it's unusual for repeating sections to do this - if only because its harder to code for. I guess there could be changes that casuse changes in the same row of the repeating section, but that again raises the question: does it actually matter if the change event fires? The attributes will be set (reset) to values you actually want them set to, and its a version change worker so it'll only happen once, so lag wont be a real issue. Typically I don't worry about setting things silently.  In this particular scenario, I need it to be set silently, because there are other things that trigger for the row of data.  In this scenario, I'm needing to set the data so it doesn't trigger, because I'm already setting the updated values (after sheetworker already triggered things, so I don't want a double-trigger, because that alters data a second time).
1636393445
GiGs
Pro
Sheet Author
API Scripter
Finderski said: GiGs said: I asked earlier if one attribute in the set was being unsilent, or if the whole set of output was. I'm not sure how to answer that question...the important pieces I need set silently aren't being set silently, I know that. You have a command that creates an output value which contains a lot of attributes, that may span multiple rows. What I'm asking, is everything in that output that has change events triggering those change events, or are the changes only being triggered from one attributre, or one row of attributes. Basically I'm trying to find out where the error is occurring: is it a particular attribute, a row of attributes in the section, or thw whole section (and thus, the whole worker)? Also, another question that just occurred to me, have you tested this on a different character? When I'm testing a lot of sheet code changes, it's pretty common for a character to stop acting properly (become corrupt), and when I change to a different character things work properly again.
1636412531
Finderski
Pro
Sheet Author
Compendium Curator
The field that are watched for changes are: repeating_skills_knowledgeother (the one I see in the logs) repeating_skills_knowledgeothermod repeating_skills_knowledgeother_delta Honestly, I don't know if that means the others are triggering silently or not, but the outcome is acting as if the delta one is triggering as well. As for the character...I create a new character, but the character I have been testing with was created fresh for this use case/test and it's been behaving the same the entire time. But I'll try a new character as well.  Unfortunately, part of the issue could be with the Transmogrifier, too...because I a need a character sheet with an older version, but I can work around that.