Updating missing variables in old saves

Twine Version: 2.39
Story Format: 2.31.1

Hello,

I am releasing my game periodically and have a bad habit of just adding variables after the fact. I have a decent amount of content so I have been asked to initialize new variables so people don’t have to replay everything from scratch.

I know it can be done via Config.saves.onLoad but my knowledge of javascript is basically nil. I’m not sure a) how to check if the variable is undefined via Javascript and b) how to then initialize it. I was thinking something along the lines of On Load If variable hammer is undefined, set hammer to 1. type deal.

Thanks for any help you can share.

1 Like

I was just about to sign off and go to bed so I haven’t tested this at all (sorry), but it looks to me like you can do this:

Config.saves.onLoad = function(save) {
	// Initialize undefined new variables in each moment of  the saved history.
	for(let i=0; i<save.state.history.length; ++i) {
		let variables = save.state.history[i].variables
		// Do something like these three lines for each new variable:
		if(typeof variables.hammer === 'undefined') {
			variables.hammer = 1
		}
	}
}

Probably someone else will give you a more careful answer…

3 Likes

Well I just tried it out on a test game and it worked great. Ill try and implement it in the full game to see how it goes but so far so good, Thanks!

Does this work with sub variables, so if I have $hammer.big or $hammer.big.red, would I have to change how its written out?

1 Like

If you want that, then you’ll need to set variables.hammer the same as you would for $hammer, and you may also need to check to see if those properties are already set on that variable or not.

This would also be a good time to implement version numbers for your releases using the Config.saves.version setting.

So you can check the version property of the Save object, or that code will be updating the hammer variable of every Save from now onward…
(untested)

Config.saves.onLoad = function(save) {
	if (save.version === 1) {
		/* Apply all the fixes required to update to version 2. */
		....
		save.version = 2;
	}

	if (save.version === 2) {
		/* Apply all the fixes required to update to version 3. */
		....
		save.version = 3;
	}
	...
}
1 Like

Sorry if its a stupid question but how do you do that. I’ve tried the loop above and it works for normal variables. But most of my variables are stuck in other variables (player.health, NPC.str, etc.). I’ve been tinkering with it but I know nothing about javascript. I assumed it would just be as easy as doing hammer.big instead of hammer, but i keep getting errors. I tried something like this but its not working (assuming that hammer already exists and I want to add big to it)

Config.saves.onLoad = function(save) {
	// Initialize undefined new variables in each moment of  the saved history.
	for(let i=0; i<save.state.history.length; ++i) {
		let hammer = save.state.history[i].variables.hammer
		if(typeof hammer.big === 'undefined') {
			hammer.big = 1
		}
	}
}

You have to define hammer as an object before you can give it object members or else it’ll throw an error like it’s doing right now.

So something like this:

Config.saves.onLoad = function(save) {
	// Initialize undefined new variables in each moment of  the saved history.
	for(let i=0; i<save.state.history.length; ++i) {
		let variables = save.state.history[i].variables
		// Do something like these three lines for each new variable:
		if(typeof variables.hammer === 'undefined') {
			variables.hammer = {};
		} 
		if(typeof variables.hammer.big === 'undefined') {
			variables.hammer.big = 1;
		} 
	}
}
1 Like

Okay, I swear I tried to do something exactly like that before and it didint work and now it is. Thank you very much, I even plugged it into my main game and its doing exactly what I want it to do. Thanks again!

How to fix the problem depends on what kinds of errors you’re seeing.

That said, this should be a more robust method which is simpler to expand:

// Initialize undefined new variables in each moment of the saved history.
Config.saves.version = 1;
Config.saves.onLoad = function (save) {
	/* isObject: Returns if a value is an object (not including "null"). */
	function isObject (Value) {
		return !!Value && typeof Value === "object";
	}
	/* isProperty: Returns if Prop is a property of the object Obj. */
	function isProperty (Obj, Prop) {
		var result = false;
		if (isObject(Obj)) {
			result = Obj ? hasOwnProperty.call(Obj, Prop) : false;
		}
		return result;
	}
	/* fixObj: Adds any missing object properties and gives a default value if it doesn't already have one set.  */
	function fixObj (varName, defaultVal) {
		var props = varName.split("."), curVar = vars, n;
		for (n = 0; n < props.length; n++) {
			if (!isProperty(curVar, props[n])) {
				if (n < props.length - 1) {
					curVar[props[n]] = {};
					curVar = curVar[props[n]];
				} else {
					curVar[props[n]] = defaultVal;
				}
			} else {
				curVar = curVar[props[n]];
			}
		}
	}

	var i, vars;
	if (save.version === undefined) {
		for (i = 0; i < save.state.history.length; ++i) {
			vars = save.state.history[i].variables;
			fixObj("hammer.big", 1);
			fixObj("hammer.tall", 2);
			fixObj("hammer.wide", 3);
		}
	}
	if (save.version < 2) {
		for (i = 0; i < save.state.history.length; ++i) {
			vars = save.state.history[i].variables;
			/* whatever fixObj() calls you need here */
		}
	}
};

Now you can just keep adding fixObj() lines for any (non-array) variables you need to fix.

Hope that helps! :grinning:

EDIT: Fixed the code per the comments below.

2 Likes

I think your problem was this line:

let hammer = save.state.history[i].variables.hammer

If hammer is already defined, setting hammer.big to 1 is fine. If hammer is undefined, saying hammer = {} will initialize it as an object, but it won’t add it to the variables object. It’ll be an object not associated with your stored variables, and so it won’t work the way that you want. If you keep referencing it as variables.hammer instead, it’ll be initialized inside the variable object like you want.

Hmm, I am running into two problems with the code.

  1. I don’t have a save version on the older saves so its undefined would that be something along if (save.version === ‘undefined’)?

  2. the code is adding just adding the variables as they are and not as part of other variables. So if I already have player, and I try to add player.health, it just adds health as a normal variable, not as a part of player. I am not sure if I am missing something in the fixObj implementation at the bottom

This line might cause an issue because arrays also return object using typeof:

return !!Value && typeof Value === "object";

I think this might protect against the issue:

return !!Value && typeof Value === "object" && !Array.isArray(Value);

Whoops. Yeah, do this:

	if (save.version === undefined) {

Don’t put the quotes around it, otherwise it will be treated as a string.

 

You didn’t show your code, so I can only guess what you’re doing, but you’d have to do something like this:

			fixObj("player.health", 100);

That assumes that $player is a generic object.

If that doesn’t work, you’ll have to tell me what kind of variable $player is and show me how you’re calling fixObj().

 

That’s true, however, if you change the code the way you did, then fixObj() would overwrite arrays with empty generic objects, which would be bad.

That said, if you want to use this to fix arrays, then we’d need to create a fixArray() function, since arrays work too differently.

Hopefully that does it. :grinning:

Ah sorry about that, it was late and forgot to send code. I am defining player like this in the Storyinit passage:

<<set $player = {
  name: "John",
  money: 0,
  level: 0,
  class: 0,
}>>

and am calling it like this

	fixObj("player.health", 10);

:man_facepalming:

My bad. I forgot one thing that had to be done. Replace the fixObj() function with this and it will work now:

	/* fixObj: Adds any missing object properties and gives a default value if it doesn't already have one set.  */
	function fixObj (varName, defaultVal) {
		var props = varName.split("."), curVar = vars, n;
		for (n = 0; n < props.length; n++) {
			if (!isProperty(curVar, props[n])) {
				if (n < props.length - 1) {
					curVar[props[n]] = {};
					curVar = curVar[props[n]];
				} else {
					curVar[props[n]] = defaultVal;
				}
			} else {
				curVar = curVar[props[n]];
			}
		}
	}

Sorry about that! (I’ve fixed the code in the earlier post as well.)

1 Like

You have nothing to be sorry for, this works great! Thanks very much for your (and everyone elses) help!