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

Two problems with API createObj. API not triggering on first create, and character sheet not updating.

March 24 (1 week ago)

Edited March 24 (1 week ago)
Chris D.
Pro
Sheet Author
API Scripter
Compendium Curator

I was trying to make some minimalist test code for the problem in thread.  Change to SetWithWorker ?

https://app.roll20.net/forum/post/12277607/change-to-setwithworker#post-12277638

When I found my new code did not display the issue being talked about but did perfectly highlight some other problems I have had. So here is some test code that highlights two bugs.   HTML

<!DOCTYPE html>
<meta charset="UTF-8">
<div>
	<input name="attr_i1" type="number" value="10" />
	<input name="attr_i2" type="number" value="10" />
	<input name="attr_i3" type="number" value="10" />
</div>
<script type="text/worker">
    // when i1 or d1 change, copy the new value into i2 or d2.
  on("change:i1 change:i2 change:i3", function test1( eventInfo ) {
        'use strict';
		log( "Sheetworker " + eventInfo[ "sourceAttribute" ] + " triggered"); 
		log( JSON.stringify(eventInfo));
  });		// This is for test1
</script>

The sheetworker simply logs if any of the values are changed. 
and .js

//
// Define a Name-space
var Earthdawn = Earthdawn || {};

// Demo8.   Log all changed attributes (prev then new). 
//    There is a sheetworker that changes copies all fields that end in an odd digit to the next field.
//    This .js copies the values of all even fields to the next field.
//    The object is to see which fields get copied. Anything put into field i1, should quickly make its way to i6. 

on("ready", function() {
  'use strict';
  log( "Demo8 ready");
  });


on("change:attribute", function (attr, prev) {
  'use strict';
  log( prev);   // use prev["name"] and prev["current"]           // {"name":"Wounds","current":0,"max":8,"_id":"-MlqexKD2f4f744TgzlK","_type":"attribute","_characterid":"-MlqeuXlNO51-RYxmJv8"}
  log( attr);   // use attr.get("name") and attr.get("current").  // {"name":"Wounds","current":"1","max":8,"_id":"-MlqexKD2f4f744TgzlK","_type":"attribute","_characterid":"-MlqeuXlNO51-RYxmJv8"}

  let sa      = attr.get( "name" ),
      cID     = attr.get( "_characterid" ),
      current = attr.get( "current" ),
      num = parseInt( sa.slice( -1 ));

  if( num == 2 ) {
    log( "API " + sa + " triggered");
    let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: 'i3' }, 20);
//    log( "before API setWW current = " + JSON.stringify( aobj ));
    aobj.setWithWorker( "current", current );
//    log( "after API setWW current = " + JSON.stringify( aobj ));
//    let bobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: 'i1' }, 20);
//    bobj.set( "current", current );
  }
}); // end  on("change:attribute"

    // Look for an object. If you can't find one, make one and return that.
Earthdawn.findOrMakeObj = function ( attrs, deflt, maxDeflt ) {
  'use strict';
  try {
//log(attrs);
    let obj,
        objs = findObjs( attrs );
    if( objs ) {
      if( objs.length > 1 ) {
        Earthdawn.errorTrace( "Error Earthdawn:findOrMakeObj() found " + objs.length + " objects: " );
        log( objs );
        let keep = 0, maxscore = 0;   // pick one to keep and get rid of the rest. 
        for( let i = 0; i < objs.length; ++i ) {
          let score = 0;
          function scoreit( a, dflt ) {
          'use strict';
            if( a !== undefined && a !== null ) {
              if( typeof a != "string" ) ++score;   // Gain points for not being string, and not being equal to default, and not evaluating to false, on the assumption that something tried to change it to those. Note that these criteria are rather arbitrary, but wanted to make decision based on something other than first or last.
              if( dflt !== undefined && a != dflt ) ++score;
              if( a ) ++score;
          } }
          scoreit( objs[ i ].get( "current" ), deflt );
          scoreit( objs[ i ].get( "max" ), maxDeflt );
          if( score > maxscore ) {
            keep = i;
            maxscore = score;
        } }
        let txt = "";
        for( let i = objs.length -1; i > -1; --i )
          if( i !== keep ) {
            txt += " attr[ " + i + " ],";
            objs[ i ].remove();
          }
        obj = objs[ keep ];
        log( "removing" + txt.slice( 0, -1) + " and keeping attr[ " + keep + "]." );
      } // end found more than one.
      else if( objs.length > 0 )
        obj = objs[ 0 ];
    } // end found one.
    if( obj === undefined && "_type" in attrs ) {   // we did not find any, create one.
      let type = attrs[ "_type" ];
      delete attrs[ "_type" ];
      obj = createObj( type, attrs);
      if( obj && deflt !== undefined && deflt !== null )
        obj.setWithWorker( "current", deflt );
      if( obj && maxDeflt !== undefined && maxDeflt !== null )
        obj.setWithWorker( "max", maxDeflt );
    }
    return obj;
  } catch(err) { Earthdawn.errorTrace( "Earthdawn:findOrMakeObj() error caught: " + err ); }
}; // end findOrMakeObj()
      // Log a programming error and give trace information.
Earthdawn.errorTrace = function( msg ) {
  'use strict';
  try {
    log( Earthdawn.timeStamp() + msg);
    if( Earthdawn.traceStack || Earthdawn.traceInfo )
      log( "Possible " + (Earthdawn.traceStack ? "TraceStack: " + Earthdawn.traceStack : "") + (Earthdawn.traceInfo ? "   TraceInfo: " + Earthdawn.traceInfo: ""));
  } catch(err) { log( "Earthdawn:ErrorTrace( " + msg + " ) error caught: " + err ); }
} // end ErrorTrace()

    // return string with timestamp in it.
Earthdawn.timeStamp = function ()  {
  'use strict';
  try {
    let today = new Date();
    return today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + " " + today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() + " (UTC)  ";
  } catch(err) { log( "Earthdawn:timeStamp() error caught: " + err ); }
} // End timeStamp

The js is not strictly minimalist, since I included a few helper routines, but the heart of the code is the on change attribute routine.

So the code is simple. If any attribute changes, log the fact and if it is i2 that changed write a trigger message and make i3 the same value.  Ignore i1 as it was there because the original sequence being tested had to have another value change as one of the things being tested. 


So here is what I found. 

New start, Browser log open, API console log open in other window. It has displayed "Demo8 ready", but nothing else.
Create a brand new character.   I1, i2, and I3 all show their default value of 10.
Increases i2 (middle field) to 12. 
browser log shows that sheetworker i2 was triggered and we see the new value of 12.
We see NOTHING new in the API log. API (on change i2) apparently was not triggered by this.

Increase i2 to 14.
Browser log shows that sheetworker i2 was triggered and new value of 14.
Now we see that the API is triggered successfully from 12 to 14.

{"name":"i2","current":"12","max":"","_id":"-OM5qekLU3_eotlocrNP","_type":"attribute","_characterid":"-OM5qcGDshiSRdor28sb"}{"name":"i2","current":"14","max":"","_id":"-OM5qekLU3_eotlocrNP","_type":"attribute","_characterid":"-OM5qcGDshiSRdor28sb"}
"API i2 triggered"
"Sheetworker i3 triggered"
"{\"sourceAttribute\":\"i3\",\"sourceType\":\"api\",\"triggerName\":\"i3\",\"newValue\":20}"
"Sheetworker i3 triggered"
"{\"sourceAttribute\":\"i3\",\"sourceType\":\"api\",\"triggerName\":\"i3\",\"previousValue\":20,\"newValue\":\"14\"}"

We also see above that the sheetworker was triggered twice, once when the API creates i3 with its default value of 20, and again when the api a fraction of a second later changes the value of i3 to be 14 to match the value of i2. This is as expected.

the api console log also this time shows only one on change i3 trigger, as it is changed once to 14. Note however that the character sheet still shows i3 with a value of 10 (which is what it displays if it does not see a database entry for i3), not the 14 that the API just set and that the sheetworker on the API thread acknowledged. The sheet is still displaying 10. 

Increase i2 to 16, 18, etc.
All further increases of i2 are identical to the last. browser log shows that it is triggered on i2 change and sees the 16.
API log is again triggering successfully with i2 changing from14 to 16.
the API console log shows one triggering of i3 being changed to match i2.
AGAIN the character sheet is still displaying i3 as being 10 even though the API has twice changed it to 16. 


If you change the character sheet from "character sheet" to "attributes and abilities" you will see all three values at their correct value (10, 16, and 16) and from then on i3 will display the value that the API is setting. But i3 had been stuck displaying its default value until the user did something to kick it into displaying as it should. 


Analysis of this sequence. 

I see two things wrong.   

(a) The very first time I changed i2 the browser console log showed an update by "player", but the API did not trigger for some unknown reason.

(b) on a character where i3 does not exist in the database, the character sheet does not display changes to i3 made by the API, until something is done to "kick" the character into knowing the attribute now exists in the database such as clicking the "attributes and abilities" tab, or some other things. 



There is a third problem that I have identified. Using the same code, if I do the  following sequence, raise i2 to 12, raise i2 to 14, raise i3 (manually) to 11, raise i2 to 16, I get the following API logs. 

{"name":"i2","current":"12","max":"","_id":"-OM5zcWKst8k7rCPnj8S","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"}

{"name":"i2","current":"14","max":"","_id":"-OM5zcWKst8k7rCPnj8S","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"}

"API i2 triggered"

"Sheetworker i3 triggered"

"{\"sourceAttribute\":\"i3\",\"sourceType\":\"api\",\"triggerName\":\"i3\",\"newValue\":20}"

"Sheetworker i3 triggered"

"{\"sourceAttribute\":\"i3\",\"sourceType\":\"api\",\"triggerName\":\"i3\",\"previousValue\":20,\"newValue\":\"14\"}"

{"name":"i2","current":"14","max":"","_id":"-OM5zcWKst8k7rCPnj8S","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"}

{"name":"i2","current":"16","max":"","_id":"-OM5zcWKst8k7rCPnj8S","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"}

"API i2 triggered"

"2025-3-24 7:12:14 (UTC)  Error Earthdawn:findOrMakeObj() found 2 objects: "

[{"name":"i3","current":"14","max":"","_id":"-OM5zdHJ29-lSDwxCTg4","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"},{"name":"i3","current":"11","max":"","_id":"-OM5ze-nXdZGPvFnDKYS","_type":"attribute","_characterid":"-OM5zaYLabc-RetTfGmP"}]

"removing attr[ 1 ] and keeping attr[ 0]."

"Sheetworker i3 triggered"

"{\"sourceAttribute\":\"i3\",\"sourceType\":\"api\",\"triggerName\":\"i3\",\"previousValue\":\"14\",\"newValue\":\"16\"}"

What seems to have happened is that when I raised i2 to 12, again nothing happened when it should have),
when I raised i2 to 14, it created i3 with a default value of 20, then changed it to 14, just like in the previous set of tests.
Then when I manually changed i3 to 11, it seems to have created a SECOND database entry for i3 with a different value. 
then when I raised i2 to 16 the code I found necessary to put in to detect this exact condition found it, and deleted one. This has been happening for years and I have mentioned it before, but this code is a minimalist set for perfectly recreating it. 


So anyway, this code demonstrates in an easily replicable way three bugs with the API.

(1) API On change attribute is not triggering for the first attribute changed.

(2) Attributes created by the API do not show as changed on the sheet when changed by the API. 

(3) When the API creates a value, and then the user (or I think a sheetworker) changes a value, the system ends up with TWO values in the database and tends to deliver random ones to the sheet and/or API lookups.


These problems should probably be fixed.

Thank you.

March 24 (1 week ago)
The Aaron
Roll20 Production Team
API Scripter

Thanks for this Repro.  If I had to guess, I'd say 2 and 3 are two parts of the same bug, which is to say that 2 is the sheet not catching creates from the API, and 3 is what happens if the sheet makes a change on a field the API has already created an attribute for.  1 I'm not quite sure on.  My guess would be it's on the sheet code side, not the API code side (frankly, the API event handing is so straight forward, it's more likely to be the event isn't getting issued), but I should be able to do some testing around that.

March 24 (1 week ago)
Jiboux
Pro
Sheet Author
Compendium Curator

Thanks Chris D. for those tests. They will certainly help in some of the issues you and me have been working on.

It is maybe me lacking some knowledge on the API side ( Chris and I work on a sheet, and I take care of the sheetworker side, and Chris of the API side ):

We also see above that the sheetworker was triggered twice, once when the API creates i3 with its default value of 20, and again when the api a fraction of a second later changes the value of i3 to be 14 to match the value of i2. This is as expected.

(4) : Why is findobj not returning i3 object ? What does it take in the HTML/Sheetworker side for an attribute to exist in the database ? I would suppose that the input declaration with a value should be enough, but it doesn't seem so. When does the object actually begin to exist for the API ? After user changed the value . After sheetworker changed the value ? After default value from the JSON ?

	<input name="attr_i3" type="number" value="10" />


March 24 (1 week ago)
Jiboux
Pro
Sheet Author
Compendium Curator

FYI on the other thread I made another series of tests that complement the observations of Chris and also recreate the initial bug I was reporting on the other thread...

Hope this helps !!!

https://app.roll20.net/forum/post/12277607/change-to-setwithworker#post-12277638

March 26 (6 days ago)
The Aaron
Roll20 Production Team
API Scripter

Declaring the value just sets the default value for the field on the character sheet.  If you reference it with @{selected|attr_i3} and it doesn't exist as an attribute object in Firebase, you will get the value assigned there.  For an attribute object to be created in Firebase, you would need to set the attribute to some value, either in a sheet worker, or in an API script.  A human could set it in the Attributes and abilities tab (possibly by creating it there) or in your sheet (to some value other than the default value) and it would create it.