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

TokenMod.ObserveTokenChange not working in game converted to Jumpgate

So, maybe this is just something that doesn't work in Jumpgate, or Jumpgate games with multiple sheets, but after a bit of searching and googling, I can't find anything about this kind of issue, so I'm hoping someone knows. I have a game with a simple API script that listens for TokenMod.ObserveTokenChange, and a macro that invokes TokenMod to apply a statusmarker.  When I use the macro, and TokenMod reports the change, my script formats a message and sends it to chat (eg, I set chained-heart using the macro to invoke tokenmod as "!token-mod --set statusmarkers&#124+chained-heart", and the log reads "CHARACTER is charmed"). This has worked fine for over a year in a pre-jumpgate game.  I created a test copy of the game in Jumpgate and enabled 2024 sheets (but am still using the 2014 sheet since I know there are issues with 2024 and scripts). In this game, if I set the marker, the script and tokenmod work fine, and the token is marked, but my script never activates (I added debug messages to log, and the handler function is just not being called at all). I've confirmed tokenmod and the script are both installed, and the macro does apply the change so tokenmod is working, and I have other scripts that issue chat log messages just fine, so I'm guessing that there's some issue with 2014 sheets in Jumpgate interfering with this form of communication (possibly due to the presence of unrelated 2024 sheets in the same game). There's a cryptic message on the console about multiple character sheets being used and that "some restrictions to the usage of API scripts have been applied", but the article that that links to says that "existing mod scripts will work with the 2014 sheet only" (but it seems to apply that simply adding other 2024 sheets doesn't affect that). It's frustrating because otherwise all of my macros and scripts are working just fine in Jumpgate. Anyone have a clue, 'cause I sure don't.
1732504574
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
That cryptic message is very confusingly worded. Basically it's telling you that if you run a macro that looks up a value on a sheet or sheets, you can't mix and match sheets in the same macro. That's because the lookup uses two different technologies underneath, and themacro will choose between them at run time. I can't answer to the larger question, but maybe The Aaron can. He'll probably want to see the code, if you can put it in a gist (or paste here if it's short enough.
1732505403
The Aaron
Roll20 Production Team
API Scripter
I just tried to duplicated this (Jumpgate Game, 2024 sheet primary, 2014 sheet secondary) and I'm getting notifications just fine.  Can you post your observing script so I can try it out?  Also, if you want to PM me and invite and GM me, I can take a look in situ.
Keith, thanks for the explanation.  That makes a lot of sense. Here's the code of the version I've been using since my last change (note that it may contain unused code, I'm not great at cleaning up once I get something working). There are commented-out loggin calls to trace usage. Also note: the "height" markers is hardcoded to a specific set of elevation markers, unless you have those, stick to the conditions). // // By: Ken S // Version: 0.0.4 // Date: 21 Apr 2024 // // This script watches for changes to token status markers and reports them. Reports are made publicly, visible to all players. // // A mapping is defined for standard 5E conditions using existing tokens (this is arbitrary, although the symbols were chosen to have some // relation to the condition where possible). When a mapping exists, that is reported, otherwise the internal name is used. // // When a token with a set condition is copied and pasted, a fresh report of the status will be generated on creation of the new token copy. // // Scripted Changes: // It is a limitation of javascript that changes created programmatically (i.e. by other scripts) will not cause change events to fire. // This script uses TokenMod's reporting function to catch changes that it performs, but changes by other scripts will not be detected. // // note: internal names are not necessarily unique, and this may not work correctly with multiple markers sharing a name. // // Because markers change immediately when the floating palette is clicked on, manual changes will always show up as single updates. However, // when TokenMod is used to change two or more markers, the changes will both be reported as one event, and these will display as a list in // a single chat message. Example (presumes this is a token macro with a selected token): // // !token-mod --set statusmarkers|+bleeding-eye // // and for a dead character, which erases all other status conditions (and implicitly makes them prone): // // !token-mod --set statusmarkers|=dead|+prone // // When using TokenMod in a macro, remember to replace the pipe (vertical bar) with &#124 as pipe is a special character in macros. // // History: // 0.0.1 = 18 Oct 2023, original // 0.0.2 = 20 Oct 2023, added TokenMod support // 0.0.3 = 17 Dec 2023, changed to report using the name on the token, not the name on the sheet // 0.0.4 = 21 Apr 2024, added conditions for height markers var tokenStatus = tokenStatus || (function() { 'use strict'; // define a mapping from the tokenMarker internal name (1st value) to the text to display for it (2nd value) // this is arbitrary and for 5E conditions and some other character statuses // note that while some of these conditions imply others (eg, a paralyzed character is also incaptacitated), that rule logic is not // included here, and must be done as part of the call to TokenMod var statusNames = [ ["bleeding-eye", "condition: blinded"], ["chained-heart", "condition: charmed"], ["ninja-mask", "condition: deafened"], ["broken-skull", "condition: freightened"], ["grab", "condition: grappled"], ["interdiction", "condition: incapacitated"], ["half-haze", "condition: invisible"], ["padlock", "condition: paralyzed"], ["white-tower", "condition: petrified"], ["skull", "condition: poisoned"], ["back-pain", "condition: prone"], ["fishing-net", "condition: restrained"], ["screaming", "condition: stunned"], ["overdrive", "condition: unconscious"], ["death-zone", "status: bleeding"], ["spanner", "status: stabilized"], ["lightning-helix", "status: hasted"], ["snail", "status: slowed"], ["Up_Marker_White_5::6021461", "height: +5"], ["Up_Marker_White_10::6021467", "height: +10"], ["Up_Marker_White_15::6021461", "height: +15"], ["Up_Marker_White_20::6021470", "height: +20"], ["Up_Marker_White_25::6021473", "height: +25"], ["Up_Marker_White_30::6021476", "height: +30"], ["Up_Marker_White_35::6021479", "height: +35"], ["Up_Marker_White_40::6021481", "height: +40"], ["Up_Marker_White_45::6021484", "height: +45"], ["Up_Marker_White_50::6021487", "height: +50"], ["Up_Marker_White_55::6021490", "height: +55"], ["Up_Marker_White_60::6021493", "height: +60"], ["Up_Marker_White_65::6021495", "height: +65"], ["Up_Marker_White_70::6021498", "height: +70"], ["Up_Marker_White_75::6021501", "height: +75"], ["Up_Marker_White_80::6021504", "height: +80"], ["Up_Marker_White_85::6021506", "height: +85"], ["Up_Marker_White_90::6021509", "height: +90"], ["Up_Marker_White_95::6021512", "height: +95"], ["Up_Marker_White_100::6021458", "height: +100"] ], version = '0.0.4', lastUpdate = 1713749357, // this is Unix Epoch in milliseconds (21 April 2024) schemaVersion = 0.1, presentTime = lastUpdate, // Unix Epoch (milliseconds), will be updated at each command invocation minArgs = 3, // count of minimum command parameters excluding the command itself ch = function (c) { var entities = { '<' : 'lt', '>' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '*' : 'ast', '/' : 'sol', ' ' : 'nbsp' }; if( entities.hasOwnProperty(c) ){ return `&${entities[c]};`; } return ''; }, // function definitions go here // logEvent // // debug function logEvent = function(newobj, prev) { log('newobj'); log(newobj); log('prev'); log(prev); }, // logEvent // sendMsg // issue a chat log message to all players sendMsg = function(msgStr) { sendChat('DM', '&{template:desc} {{desc=' + msgStr + '.}}'); }, // sendMsg // getStatusNames // // see if there's a mapping defined, and if so return the mapped name, otherwise return the internal name getStatusNames = function(markerName, msg) { let index = []; index = statusNames.filter( function(el) { return !!!el.indexOf(markerName); }); if ((index == null) || (index.length == 0)) { msg = msg + markerName; } else { msg = msg + index[0][1]; } return(msg); }; // getStatusNames // reportStatusChange // // report to the chat log what changed reportStatusChange = function(obj, added, removed) { let msg = "Status changed: "; let i, len = 0; let newList = true; let doingBoth = false; //let index = 0; //log("in TS RSC: "); //log(statusNames); //log(obj); //log(obj.get("represents")); let char = getObj('character', obj.get('represents')); //log(char); let tokenName = obj.get('name'); //log(tokenName); if (tokenName != '') { msg = msg + tokenName + ": "; // normally report the name applied to the token } else { msg = msg + char.get('name') + ": "; // report the owning character name of the token } // append the list of added status elements i = 0; len = added.length; newList = true; while (i < len) { if (added[i] != "") { if (newList) { msg = msg + "is ("; newList = false; } else { msg = msg + ', '; } msg = getStatusNames(added[i], msg); doingBoth = true; } i++; } if (!newList) { msg = msg + ") "; } // append the list of removed status elements i = 0; len = removed.length; newList = true; while (i < len) { if (removed[i] != "") { if (doingBoth) { msg = msg + ', '; doingBoth = false; } if (newList) { msg = msg + "is no longer ("; newList = false; } else { msg = msg + ', '; } msg = getStatusNames(removed[i], msg); } i++; } if (!newList) { msg = msg + ") "; } sendMsg(msg); }, // reportStatusChange // HandleChange // Select the reporting function based on what changed. // // Keep this as lightweight as possible HandleChange = function (newobj, prev) { let newName = ''; let reporting = false; let added = []; let removed = []; //log('in TS HC: '); //logEvent(newobj, prev); if (!newobj || !prev) { log('TM: error, newobj or prev undefined.'); return; } if (newobj.get('statusmarkers') == prev.statusmarkers) return; // it didn't change, so bail // normalize the two into arrays let prevstatusmarkers = prev.statusmarkers.split(","); let statusmarkers = newobj.get('statusmarkers').split(","); //log(statusmarkers); //log(prevstatusmarkers); statusmarkers.forEach(function(newMarker){ if ((newMarker != "") && !prevstatusmarkers.includes(newMarker)) { //sendMsg("TS: added " + newMarker); added.push(newMarker); reporting = true; } }); prevstatusmarkers.forEach(function(newMarker){ if ((newMarker != "") && !statusmarkers.includes(newMarker)) { //sendMsg("TS: removed " + newMarker); removed.push(newMarker); reporting = true; } }); if (reporting) { reportStatusChange(newobj, added, removed); } }, // HandleChange // checkInstall // This code runs when the script is installed to the sandbox or on sandbox restart (game load, etc), ie not often. // Purpose is to provide a log message that the code loaded okay, and update any non-transient storage as needed. checkInstall = function () { log('-=> tokenStatus v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); log(' > no local info preserved in state <'); // no state needed at present, place code to restore here if we use it }, // checkInstall // registerEventHandlers // on chat.message: normally invoked by the user typing the command in chat // on change:attribute: invoked by a chang to an attribute on a character sheet // if we wanted to listen for other events (like changes to a token) that would go here too. registerEventHandlers = function () { //on('chat:message', HandleInput); on('change:graphic:statusmarkers', HandleChange); // function(obj, prev) // events do not fire for API-driven changes, but TokenMod supports an event reporting mechanism, so use it to catch TokenMod changes if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(HandleChange); } //TokenMod watcher /* if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(function(obj,prev){ if(obj.get('statusmarkers') !== prev.statusmarkers){ sendChat('Observer Token Change','Token: '+obj.get('name')+' has changed status markers!'); } }); } //TokenMod watcher */ }; // the following maps externally visible names (the first name) to internal functions (the second name) return { checkInstall: checkInstall, registerEventHandlers: registerEventHandlers }; }()); // on ready: When the script is loaded, these are called. (game is completely loaded by then) on("ready",function () { 'use strict'; tokenStatus.checkInstall(); tokenStatus.registerEventHandlers(); }); // on // tokenStatus and here is the macro to set a condition: ?{Add Condition|Blinded,!token-mod --set statusmarkers&#124+bleeding-eye|Charmed,!token-mod --set statusmarkers&#124+chained-heart|Deafened,!token-mod --set statusmarkers&#124+ninja-mask|Frightened,!token-mod --set statusmarkers&#124+broken-skull|Grappled,!token-mod --set statusmarkers&#124+grab|Incapacitated,!token-mod --set statusmarkers&#124+interdiction|Invisible,!token-mod --set statusmarkers&#124+half-haze|Paralyzed,!token-mod --set statusmarkers&#124+padlock&#124+interdiction|Petrified,!token-mod --set statusmarkers&#124+white-tower&#124+interdiction|Poisoned,!token-mod --set statusmarkers&#124+skull|Prone,!token-mod --set statusmarkers&#124+back-pain|Restrained,!token-mod --set statusmarkers&#124+fishing-net|Stunned,!token-mod --set statusmarkers&#124+screaming&#124+interdiction|Unconscious,!token-mod --set statusmarkers&#124+overdrive&#124+interdiction|Hasted,!token-mod --set statusmarkers&#124+lightning-helix|Slowed,!token-mod --set statusmarkers&#124+snail|Bleeding,!token-mod --set statusmarkers&#124=death-zone&#124+back-pain|Stabilized,!token-mod --set statusmarkers&#124+spanner&#124-death-zone|Dead,!token-mod --set statusmarkers&#124=dead&#124+back-pain} and to clear it: ?{Remove Condition|Blinded,!token-mod --set statusmarkers&#124-bleeding-eye|Charmed,!token-mod --set statusmarkers&#124-chained-heart|Deafened,!token-mod --set statusmarkers&#124-ninja-mask|Frightened,!token-mod --set statusmarkers&#124-broken-skull|Grappled,!token-mod --set statusmarkers&#124-grab|Incapacitated,!token-mod --set statusmarkers&#124-interdiction|Invisible,!token-mod --set statusmarkers&#124-half-haze|Paralyzed,!token-mod --set statusmarkers&#124-padlock&#124-interdiction|Petrified,!token-mod --set statusmarkers&#124-white-tower&#124-interdiction|Poisoned,!token-mod --set statusmarkers&#124-skull|Prone,!token-mod --set statusmarkers&#124-back-pain|Restrained,!token-mod --set statusmarkers&#124-fishing-net|Stunned,!token-mod --set statusmarkers&#124-screaming&#124-interdiction|Unconscious,!token-mod --set statusmarkers&#124-overdrive&#124-interdiction|Hasted,!token-mod --set statusmarkers&#124-lightning-helix|Slowed,!token-mod --set statusmarkers&#124-snail|Bleeding,!token-mod --set statusmarkers&#124-death-zone|Stabilized,!token-mod --set statusmarkers&#124-spanner|Dead,!token-mod --set statusmarkers&#124-dead} It requires the TokenMod script, obviously. The Aaron: I will PM you an invite to my test game.