Advertisement Create a free account

[Script] TokenSync - Synchronize changes to tokens

1458247906

Edited 1458259291
James W.
Pro
Sheet Author
API Scripter
Here's the latest script I've been working on: a script to synchronize changes to a token to all other tokens that represent the same character .  Now you can set the tokens representing your PCs to always have the same status markers, for example.  Just tell TokenSync which property (or properties) to synchronize, and any time that property changes on any token that represents that character changes, TokenSync will propagate that change to all other tokens that represent that character. Usage: !tokensync --add property[|properties] Adds the specified property to the list of properties that TokenSync will synchronize for the character the selected token represents; if you want to specify multiple properties, just separate them with a vertical bar ("|"). !tokensync --remove property[|properties] Removes the specified property from the list of properties that TokenSync will synchronize for the character the selected token represents; if you want to specify multiple properties, just separate them with a vertical bar ("|"). !tokensync --removeall Removes all properties for the selected character from the sync list. !tokensync --forcesync Forces a synchronizes event to update all properties to match selected token; useful for forcing updates after calling an API script to change token properties. Properties that can be syncrhonized: imgsrc, bar1_link, bar2_link, bar3_link, width, height, rotation, layer, isdrawing, flipv, fliph, name, aura1_radius, aura2_radius, aura1_color, aura2_color, aura1_square, aura2_square, tint_color, statusmarkers, showname, showplayers_name, showplayers_bar1, showplayers_bar2, showplayers_bar3, showplayers_aura1, showplayers_aura2, light_radius, light_dimradius, light_otherplayers, light_hassight, light_angle, light_losangle, light_multiplier Note: imgsrc will only sync if the token was created from your library; basically, if ChangeTokenImage.js can change the image, TokenSync can synchronize the change. API scripts: Because API scripts making changes to a token doesn't result in change events, TokenSync won't automatically synchronize any token changes caused by an API script.  To get around this, there are two options. Manually force a sync event by calling "!tokensync --forcesync". Update the API script to call an update ("tok" is token object with the updated properties, "property" is the property to synchronize; you can synchronize multiple properties by separating them with vertical bars): if (!_.isUndefined(TokenSync)) TokenSync.syncProperty(tok, property); Since this checks to see if TokenSync is defined, it's safe to call this in other scripts, even if the user isn't using TokenSync.
1458249586
The Aaron
Forum Champion
API Scripter
Neat idea!  I'm definitely going to check this out!
1458249928
James W.
Pro
Sheet Author
API Scripter
TokenMod would be the most obvious candidate for TokenSync integration... :P
1458256038
The Aaron
Forum Champion
API Scripter
Indeed. =D
1458259380
James W.
Pro
Sheet Author
API Scripter
Well, took care of the TODO list; new tokens will now immediately sync against the existing tokens, deleted characters are removed from the sync list, and as an added bonus, changing what character a token represents forces an immediate sync.
1458259542
The Aaron
Forum Champion
API Scripter
I've got some feedback for you, if you're interested...
1458260451
James W.
Pro
Sheet Author
API Scripter
Always.
1458261876
The Aaron
Forum Champion
API Scripter
Cool. • It seems like syncAll is redundant, particularly since everywhere you use it, you immediately split it on "|", resulting in an array identical to syncable. • syncProperty() does a findObjs() for each property it is setting resulting in the same set of objects.  You could do that once and store it in a variable. • syncProperty() individually sets each property to the new value.  Since you're setting the same value on each token, you could construct an object with all of the settings and just pass that once to the .set() function instead. • I think the way you're storing properties in state might be getting in your way.  I can see how you might have gotten here by initially planning to just have a shallow set of properties that were synced on all represents tokens, then decided to subdivide by character.  That was a good idea, but now you have a structure like this: { 'prop1': ['charid1','charid2','charid3'], 'prop2': ['charid1','charid3'], 'prop3': ['charid2','charid3'], 'prop4': ['charid1','charid2'] } However, all of your operations start with a character id and get the list of properties to set for it (or rather, look for properties that contain that character id).  If you flip the representation, I think it will simplify many things about the code: { 'charid1': ['prop1','prop2','prop4'], 'charid2': ['prop1','prop3','prop4'], 'charid3': ['prop1','prop2','prop3'] } Now syncProperty() can use: _.each(_.intersection(propList,(state.TokenSync.syncList[updatedToken.get('represents')]||[])), /*...*/ ); Adding new properties to a character is just (with some prep for the propList array): state.TokenSync.syncList[charID] = _.union( (state.TokenSync.syncList[charID]||[]), propList); Remove is just (deleting an undefined property is not an error): delete state.TokenSync.syncList[charID]; etc. That's probably a good start...
1458264542
James W.
Pro
Sheet Author
API Scripter
Just as a quick explanation as to why I did things the way I did, the reason I made syncList property-orientated instead of character-orientated was to help with rebuilding the event handlers on API restarts.  That said, looking over the Underscore.js library (not terribly familiar with it), it looks like I can rebuild the event handlers pretty quickly using _.values() and _.uniq(). Remove is designed to be able to allow a player to deregister individual properties, so it kinda needs to be more than just deleting the character from the list, but otherwise that would be the easier way to handle --removeall and the last sync'd property on a character. Very helpful; I'll work on these soon.
1458266272
The Aaron
Forum Champion
API Scripter
All, that makes sense, this would get you all the properties: _.uniq(_.flatten(_.values(f))); Remove just the one property: state.TokenSync.syncList[charID] = _.without( (state.TokenSync.syncList[charID]||[]), property); Cool.  Just suggestions of course. 
1458279467
James W.
Pro
Sheet Author
API Scripter
Good suggestions, though.  Finished updating to the new schema; v1.5 ended up being 26 lines shorter than v1.1.
1458279544
The Aaron
Forum Champion
API Scripter
Sweet!!  :)
1458306700
The Aaron
Forum Champion
API Scripter
This is looking really good!  I like the features you're building and the way you decompose the problem.  =D You obviously have a good bit of practical programming experience already so here are a few more observations, mostly centered around idiomatic javascript.    You can actually simplify syncProperty() further with a bit of idiomatic javascript:     syncProperty = function(updatedToken, properties) {         var propList = _.intersection(                 (properties && properties.split && properties.split("|")) || [],                 state.TokenSync.syncList[updatedToken.get("represents")] || []             ),             update = _.reduce(propList,function(m,p){                 m[p]=updatedToken.get(p);             },{});         _.each(findObjs({ _subtype: "token", represents: updatedToken.get("represents")}),function(t){             t.set(update);         });     }, Broken down: (properties && properties.split && properties.split("|")) || [], This checks to see if properties is "truthy" and has a .split and calls split, or uses [].   Building an update object with _.reduce() means you don't need to loop over the properties for each token, making the update an O(n) operation, rather than O(n 2 ). If the token doesn't represent a character, the proplist ends up being empty, which causes the update to be {} and the list of characters that match the id ends up as [].  You could return before the _.each() if propList.length is 0 to avoid the findObjs().     add = function(charID,properties,silent) {         var propsRequested = properties.split("|");         var propsRejected = _.difference(propsRequested, syncable);         var propList = _.intersection(propsRequested, syncable);         var propsAlready = _.intersection(propList,(state.TokenSync.syncList[charID]||[]));         propList = _.difference(propList,(state.TokenSync.syncList[charID]||[]));         if (!silent) {             if (propsRejected.length) {                 sendChat("TokenSync","Invalid sync propert"+(1===propsRejected.length ?'y':'ies')+": " +propsRejected.join());             }                              if (propsAlready.length) {                 sendChat("TokenSync","Already synchronizing: " + propsAlready.join());             }                          if (propList.length) {                 sendChat("TokenSync","Now synchronizing: " +propList.join());             }         }         state.TokenSync.syncList[charID] = _.union( (state.TokenSync.syncList[charID]||[]), propList);         _.each(propList,registerListener);     }, Sometimes it's hard to see the forest for the trees. =D  The .join() method on Array defaults to joining the contents together with ", " as the separator, which visually simplifies this.  Also using the ternary operator to pick the singular/plural ending means you can do the output in one line, though I hardly think anyone would fault you for just using the plural all the time. It's a bit more idiomatic in javascript just use the truthy/falsy nature of variables in logic tests, so !silent rather than silent !== true , and propList.length , rather than propList.length > 0 . I wonder if it might be simpler to simply register a single on('change:token',...), rather than keeping track of all the different propsListened.  You'll already be attempting to sync every listened property on each token regardless of it that token is syncing that property so at worst, you attempt no more.  However you change from doing O(n) calls to syncProperty() on each token change to only doing O(1) calls and you don't have to track what properties you've registered for.  (and there isn't a way to unregister, to answer you comment question. =D). Speaking of propsListened, you don't actually need to put that in the state.  Since you're setting it to [] in checkInstall(), its lifetime will always be the lifetime of the module closure so you could put it after lastUpdate.  That will also remove the crash bug when it tries to set it before state.TokenSync has been created 2 lines lower. This isn't shorter (largely due to formatting), but you might like it as an alternative way to handle syncNewToken():     syncNewToken = function(newTok) {         var done = false,             charID = newTok.get('represents');         filterObjs(function(o){             if( !done &&                 'graphic'===o.get('type') &&                 'token'===o.get('subtype') &&                  o.get('represents') === charID &&                 o.id !== newTok.id )             {                 syncProperty(o);                 done = true;             }             return false;         });     }, filterObjs() is usually used to get a set of objects when you have more dynamic comparisons you need to perform.  However, it's also a good way to perform an operation using the list of objects.  It's slightly more efficient (according to Riley) than using findObjs() followed by filtering the list of returned objects.  (It would be nice if we had a findObj() with a finish on first match...).  Anyway, the first time this finds a token that represents the character but isn't the new token, it will sync and then just return false the rest of the time. Cheers! Disclaimer: I've not run any of the above code, just linted it.
1458321738
James W.
Pro
Sheet Author
API Scripter
(properties && properties.split && properties.split("|")) || [], should probably be: (properties && properties.split && properties.split("|")) || syncable, ...since we want to sync everything when we don't get a properties list, and you forgot to return the memo in _.reduce(); otherwise, spot on.  Also, I added a quick check to see if update contains anything; if not, there's no reason to continue. The reason I'm making an event handler for individual properties changing is because I didn't want to fire off a sync when properties we don't care about (such as non-syncable properties like left and top, as well as properties that the user hasn't asked to sync) change.  So the question here is which is preferable, syncing properties one by one as the events fire, or syncing everything every time anything on the token changes.  Since the event handlers only kick in off of manual user input, we're only ever going to fire off one or two events at once (resizing the token is the only way I know of to change two properties at once), so I'm thinking it might be better to continue with individual event handlers, rather than forcing a full sync every time the token moves. Here's the latest version ; I can't commit it to my repo until I get home.
1458323970
The Aaron
Forum Champion
API Scripter
Good point on syncable, that probably fell out on a previous bit of code I suggested. =D  I'm forever forgetting to return the memento, so that doesn't surprise me.  That's also a good point about the single property updates.  Given that, I retract my former suggestion. =D
1458325692
James W.
Pro
Sheet Author
API Scripter
Thanks for the suggestions; I think I learned plenty of stuff to help me with HL-Import.
1458327495
The Aaron
Forum Champion
API Scripter
Cool!  Very happy to help!
1458510235
James W.
Pro
Sheet Author
API Scripter
TokenSync v1.6 committed.