G G is pretty much right.
About Versioning
The standard I try to follow is Semantic Versioning ( https://semver.org/ ). The basic idea is:
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards-compatible manner, and
- PATCH version when you make backwards-compatible bug fixes.
I'm not very good at applying that the "right" way. What I generally do is bump the PATCH number whenever I fix things, or when I make tweaks to existing thing. If I add big new things, I'll bump the MINOR number (and reset the PATCH to 0). If I rewrite from scratch, possibly fundamentally changing the script, I'll bump the MAJOR number and reset the other to 0.
I think it's obvious why Versioning is useful, but it's especially useful when you've released scripts for other people to use. You might join someone's game to help them and find they've got a version from 3 years ago, and that's why the latest features in your script aren't working for them. =D
About Time
lastUpdate is the last time I saved the script. You mention external editors, and this is one of the reasons I use one! I've configured my editor of choice (Vim, cuz it's the best. (Shut it, Brian! =D )) to find the lastUpdate variable in any Javascript file I save, and set the current time to it. Most programming languages store time as an integer number of seconds since "The Epoch". The Epoch is the "Thursday, January 1, 1970 12:00:00 AM GMT". (This is why Y2K wasn't as big a deal as the media made it out to be... but watch out for Tuesday, January 19, 2038 3:14:07 AM GMT...) This integer number of seconds format is usually referred to as a Unix Timestamp.
If you're curious, you can try some conversions at: https://www.epochconverter.com/
Javascript's Date class deals with time in the millisecond resolution, instead of seconds, which is why I multiple by 1000 before converting it to a human readable format. The Date class is perfectly happy to deal with a text format, so you could just use something human readable:
const lastUpdate = '2018-07-21 9:56:20 am';
/* ... */
log('-=> rmptutor v'+version+' <=- ['+(new Date(lastUpdate))+']');
Make sure it's 'YYYY-MM-DD hh:mm:ss Me' so the month and day don't get swapped.
Together with version, lastUpdate provides useful information to the humans using the script:
"-=> TokenMod v0.8.40 <=- [Tue Jul 17 2018 01:03:56 GMT+0000 (UTC)]"
"-=> TurnMarker v1.3.8 <=- [Tue Jul 18 2017 20:10:57 GMT+0000 (UTC)]"
"-=> AaronDebug v0.1.0 <=- [Sun Apr 22 2018 13:54:54 GMT+0000 (UTC)]"
"-=> CharEdit v0.1.1 <=- [Wed Feb 08 2017 14:13:10 GMT+0000 (UTC)]"
"-=> MoveLog v0.1.0 <=- [Fri Jun 29 2018 21:45:44 GMT+0000 (UTC)]"
"-=> APISelection v0.1.0 <=- [Tue Jul 17 2018 02:15:35 GMT+0000 (UTC)]"
About schemaVersion
One of the most important things I did early on was come up with writing a schemaVersion into the state. Where the above version is meant for humans to understand, the schemaVersion is meant for the script to understand. As G G alluded to, it doesn't version the script at all, it versions the schema (as in layout, pattern, structure, etc) of the state object itself, more specifically the part of the state that this script deals with. The checkInstall() function's purpose is to compare the schemaVersion in the script to the one stored in the state, and make adjustments as needed so that the script can function.
The simplest adjustment is just replacing the state with the new format, but you can do much better! GroupInitiative is a great example of this:
const checkInstall = function() {
log('-=> GroupInitiative v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
if( ! _.has(state,'GroupInitiative') || state.GroupInitiative.version !== schemaVersion) {
log(' > Updating Schema to v'+schemaVersion+' <');
switch(state.GroupInitiative && state.GroupInitiative.version) {
case 0.5:
state.GroupInitiative.replaceRoll = false;
/* break; // intentional dropthrough */ /* falls through */
case 0.6:
state.GroupInitiative.config = {
rollType: state.GroupInitiative.rollType,
replaceRoll: state.GroupInitiative.replaceRoll,
dieSize: 20,
autoOpenInit: true,
sortOption: 'Descending'
};
delete state.GroupInitiative.replaceRoll;
delete state.GroupInitiative.rollType;
/* break; // intentional dropthrough */ /* falls through */
case 0.7:
state.GroupInitiative.config.announcer = 'Partial';
/* break; // intentional dropthrough */ /* falls through */
case 0.8:
state.GroupInitiative.config.diceCount = 1;
state.GroupInitiative.config.maxDecimal = 2;
/* break; // intentional dropthrough */ /* falls through */
case 0.9:
state.GroupInitiative.config.diceCountAttribute = '';
/* break; // intentional dropthrough */ /* falls through */
case 0.10:
if(_.has(state.GroupInitiative.config,'dieCountAttribute')){
delete state.GroupInitiative.config.dieCountAttribute;
state.GroupInitiative.config.diceCountAttribute = '';
}
if(_.has(state.GroupInitiative.config,'dieCount')){
delete state.GroupInitiative.config.dieCount;
state.GroupInitiative.config.diceCount = 1;
}
/* break; // intentional dropthrough */ /* falls through */
case 1.0:
state.GroupInitiative.savedTurnOrders =[];
/* break; // intentional dropthrough */ /* falls through */
case 'UpdateSchemaVersion':
state.GroupInitiative.version = schemaVersion;
break;
default:
state.GroupInitiative = {
version: schemaVersion,
bonusStatGroups: [
[
{
attribute: 'dexterity'
}
]
],
savedTurnOrders: [],
config: {
rollType: 'Individual-Roll',
replaceRoll: false,
dieSize: 20,
diceCount: 1,
maxDecimal: 2,
diceCountAttribute: '',
autoOpenInit: true,
sortOption: 'Descending',
announcer: 'Partial'
}
};
break;
}
}
};
By using a switch statement with cases that fall through, it picks the point where the stored schema is and applies all the changes necessary to bring it up to the current version. This is important because someone might have last used the script when the schema version was vastly different, so merely making the changes from "last time" isn't sufficient.
And all these changes preserve the configuration and running state for the user. Imagine how annoying it would be if updating the Bump script meant that it lost you color choices for the GM layer and Object layer tokens, and forgot all of the tokens who have been Bump'd!!
Having this schemaVersion concept also frees you up to change bad decisions you made earlier in development. If you realize that "and array of token ids" isn't sufficient, and you really wanted "a key/value mapping from page id to an array of token ids", you can seamlessly and easily change that.
Most of the cases in my checkInstall() switch statement correspond to schemaVersion numbers and fall through to the cases below them, but there are two special cases:
- The 'UpdateSchemaVersion' case will only come up when it gets fallen into by the above cases. It serves 2 purposes: 1) write the current schemaVersion into the state object and 2) mark where the next schema version case should go, right above it.
- The default case is what gets hit when the script has never been installed (or the schemaVersion matches nothing). It writes the full schema to the state. Be sure when you add a new schemaVersion that you update this object to match what the current schema should be.
That should about cover those 3 lines. =D What's next?