- Jun 10, 2017
- 11,009
- 16,295
last update - 06 November 2019
All authors have to deal with the same problem, at some point of the development of their game, they have to add new variables. Some deal correctly with this. But some seem to have difficulties to do it, and the addition lead to incompatibility with saved files made from a previous version of the game.
But there's no fatality, it is possible to add new variables to a Ren'py game and still keep the save compatibility. There even many ways to do it, each one having it's pros and cons.
A - Variable addition:
Whatever the player start a new game, or load a saved game, "myNewVariable" will be created by Ren'py.
But there's a pitfall because the variables created in an init block are not saved by default. It's not a problem for regular/scalar-like values (number, boolean, string). Either the value haven't been changed yet, and so the default value is the actual value, or the value have been changed, and they are made savable just by this.
Still, it's a problem if the variable is an object, because they will never been saved. Therefore, use this method with caution.
Like for the init block above, the variable will be created whatever the player start a new game or use a save file. But here the variable will be automatically made savable, unlike for the init block method.
It's also the official way to proceed, but not necessarily the better one.
Obviously, this method is only valid if your game follow a linear path, or if the different routes join regularly enough. In this case, just add the variables in the first label encountered in the new update. Like all the players will cross this point, and cross it at the same moment in the game, whatever it's from a new game or a from a saved file, the variables will be created and savable.
In the same way, if you split your game by days, and each day start with an unique label (like in my example above), you can add the variables in this label. In this particular case I even recommend this method over the official one. It will be more clear for many authors, especially if they aren't good at codding. Each variable regarding the current day are created at the start of the day. Therefore they'll be easy to find, whatever it's to check the default value, verify the syntax, or some other reason.
When it exist, the after_load label is automatically called by Ren'py each time a saved file is loaded, but only when a saved file is loaded. This mean that the variables will not be created when the player start a new game. Because of this, you'll need to declare the variables twice:
As you can see, this is not the more suitable way to add variables in the game. Still I present it because it offer a possibility which isn't present with the other method... By using the after_load label, you can alter the variables already created and the value they had in the save file. But first, I'll address the main problem of this method.
With the time and the updates, you'll add many variables, which will lead to many lines in the after_load label. There's also the problem of
Just update the
Note that there's a work around to get ride of the necessity to have the variables addition in both the start label and the after_load label. It's to put the creation in an independent label called from both labels.
But always keep in mind that
Else, you'll either have a game that will start with higher values than expected (if you make error corrections in the updateVariables label by example, see below), or you'll constantly reset the variables (if you don't ensure first that the variable is effectively absent).
Thanks to tacito for the idea.
B - Variable alteration:
Now, like I said above, the main advantage of after_load is that it let you alter the value of existing variables present in the save file. This let you cover change in the content of the game already covered by the previous updates, as well as correct a bug present in a previous update.
Imagine that you have a variable named
That's where the after_load label show it's full utility:
When the player will load a saved file,
Side note: In this particular case, a boolean to an integer, in fact it cause few problems. Python will understand what you try to do, and change "False" to "0", and "True" to "1". But it was the only example which crossed my mind.
In the same way, imagine that you changed your game by adding 5 new
Here again, the after_load label come to your rescue:
With this, a player using a saved game will have the 5 points you added in the part he already played. So, he will be able to have the 15 points needed by the condition.
In the example I gave the full number of points, it's up to you to do this or assume that the player picked some wrong choice, and so add only half the number of points. It all depend of the way your game works. If the player need to always have the full number of points, be kind and give him all the added points.
The same let you decrease a variable value if you decide to remove some points. But in this case you must act with more caution, to not let the player be stuck because of your correction.
Increasing points is easy. You can easily assume the choice made by the player without risk to let him be stuck because of a lack of points. Like I implied above, add all the points if the player need to always be full points, else adding just half of the points is enough most of the time. But for a points reduction it's more difficult since you don't know if the player have the removed points or not, nor how many of them he have. In consequence, you need to do a little more works here.
The best case is when the player need to always have all the points, since you just have to take away the number of points you removed from the game. But in any other case you need to know your game and speculate in regard of the actual number of points. The average way to do is to split the number of points in five parts:
Also note that after_load is a regular label. So you can put Ren'py statements in it. By doing this, you can also interact with the player.
Imagine, in your game the MC cross a real bad guy who talk badly to his girl. Then, the player have the possibility to punch him. At first, you saw this as a simple event, but later you thought that it can be a good idea to keep track of the player action. So, you added a
But, how to deal with it in a saved file ? The default option is to force the variable to
new content :
There's an alternate way to do this, without the need of a
For this, you need to declare the variable by using the
Note that this can not be done in the after_load label. Indeed, in this label you have no way to know if the value of
As you can imagine, having to ask the player for his previous choices can't really works if you suddenly add a bunch of variables. It's more an emergency solution than a suitable one. But if you planed it from the start of your game, you have a solution which can drastically reduce the number of questions, even if you add many variables. For this, you need to track the labels passed through by the player. It's done with the help of the
Now, if during his current play the player passed through a given label, you'll find the name of this label into the
As for the use, imagine the following situation: In your game, there's two encounters with Mia, and the possibility to punch, or not, a bad guy. Like above, you want to now count the number of encounter with Mia, instead of the knowing that there was at least one, and you now want to keep track of the reaction of the player when facing the bad guy.
For the encounter with Mia it's easy, the two encounters have their own label ("miaFirstEncounter" and "miaSecondEncounter"). But for the bad guy it's more complicated. Like the MC is the hero, he knocked it down in one punch, and like he's also cool, he just continued walking after this. It mean that there isn't a particular label for you to know the choice made by the player. In the same time, it's an event which happen in an optional path, so you don't want to spoil the story by asking the player for a choice he perhaps don't had to make.
The solution looks like this:
The correction of
As for the bad guy, it's a little more complicated. As previously, you'll ask the player for his choice, but this time you'll do it only if he played the optional event where the choice happen. Then, whatever the player had the possibility to punch him or not, you mark the game as patched, since it's effectively the case. The fact that the patch didn't had to be applied doesn't mean that it will be needed in the future. If the player finally follow this optional path, he will do it with your new code, which now directly change the value of
Note that this solution isn't limited to these two examples. It can also let you know if the player passed through a label where you added or removed points, and so if this addition/removal should apply to him or not.
We are all human, and because of this we all make errors sometimes. When you're a game coder, these errors can have big consequences and make two players face different reaction of the game, while they still have done the same actions. This simply because one played a bugged update, will the other played after you corrected the bug. By chance, here again the after_load label can come to your rescue.
Imagine that you where really tired when writing your last update and that you made two errors. The first one was to write "numberofpoints" instead of "numberOfPoints". Python and Ren'py being case sensitive, those are two different variables. Your second error was to write
The consequences aren't small one. Not only anyone who've played this update before the correction will not benefit from the points added in it, but also instead of having encounterMia = 3, he will have encounterMia = 1. But don't worry, there's a solution:
Firstly you correct the number of encounter with Mia, but only if the player chose to encounter her. Secondly you take the value of the bogus variable
Well, now you have no excuses. As long as you don't completely change the mechanism of your game, it can be save compatible most of the time. This whatever the variables you add in a new update, and whatever the way you changed the use of the existing variables. It can even be saved compatible if you add mandatory points/whatever in the parts already played, or if you finally decide to track some choice made by the player.
Even more importantly, you now have a way to correct many of the variable related bugs of an update without forcing the player to start a fresh new game.
Side Notes :
As discussed in the thread, the second cause of save incompatibility is when you play too much with the label's name. It's a little out of the subject, but still I decided to give it a quick cover to remove the blur.
Ren'py works with a strong mechanism of save loading, mostly relying on three things :
As long as you didn't moved the label and it's content, or didn't deleted the .rpyc file, Ren'py will have almost no problems to load a save file. Remove lines in the label, Ren'py will load at the correct place. Add some lines, at worst Ren'py will load few lines before the one where the game was saved.
Problems can only come when you start to do more challenging operations ; like deleting the .rpyc file, moving the label, and its content, to another file, or renaming the label. But you must understand that the problems will effectively happen only if you do at least two of this at the same time. Therefore :
It doesn't mean that you should do one of this things ; especially renaming a label, since you can forget to change a jump or a call statement to it. The fact that this will not break your game doesn't mean that it will not lead to inconveniences to the player.
But if really you haven't the choice and found yourself in the obligation to do so, at least you'll now know what is possible and what isn't.
As demonstration, I included a small "
All authors have to deal with the same problem, at some point of the development of their game, they have to add new variables. Some deal correctly with this. But some seem to have difficulties to do it, and the addition lead to incompatibility with saved files made from a previous version of the game.
But there's no fatality, it is possible to add new variables to a Ren'py game and still keep the save compatibility. There even many ways to do it, each one having it's pros and cons.
A - Variable addition:
1 - In an init block:
Python:
init python:
myNewVariable = False
But there's a pitfall because the variables created in an init block are not saved by default. It's not a problem for regular/scalar-like values (number, boolean, string). Either the value haven't been changed yet, and so the default value is the actual value, or the value have been changed, and they are made savable just by this.
Still, it's a problem if the variable is an object, because they will never been saved. Therefore, use this method with caution.
2 - By using the
You must be registered to see the links
statement:
Python:
default myNewVariable = 1
label start:
[...]
It's also the official way to proceed, but not necessarily the better one.
3 - At the start of the update:
Python:
label day12:
$ day12_firstVariable = False
In the same way, if you split your game by days, and each day start with an unique label (like in my example above), you can add the variables in this label. In this particular case I even recommend this method over the official one. It will be more clear for many authors, especially if they aren't good at codding. Each variable regarding the current day are created at the start of the day. Therefore they'll be easy to find, whatever it's to check the default value, verify the syntax, or some other reason.
4 - By using both the start and after_load labels:
When it exist, the after_load label is automatically called by Ren'py each time a saved file is loaded, but only when a saved file is loaded. This mean that the variables will not be created when the player start a new game. Because of this, you'll need to declare the variables twice:
Python:
label start:
# Variables from 0.01 version.
$ someOldVariable = False
# Variables from 0.11 version.
$ variablePreviousUpdate = False
# Variables from 0.12 version.
$ myNewVariable = False
$ yetAnotherVariable = False
$ codeVersion = 0.12
[...]
label after_load:
if codeVersion < 0.11:
$ variablePreviousUpdate = False
$ codeVersion = 0.11
if codeVersion < 0.12:
$ myNewVariable = False
$ yetAnotherVariable = False
$ codeVersion = 0.12
return
With the time and the updates, you'll add many variables, which will lead to many lines in the after_load label. There's also the problem of
codeVersion
which need to be a number; here I use a float, but it can also be an integer (012). So, when you'll patch your game to the version 0.12a, there will be some issues. One way to deal with this is to let Python do the works for you:
Python:
label start:
# Variables from 0.01 version.
$ someOldVariable = False
$ variableList = [] # empty by default since there's no possible saved files yet.
# Variables from 0.11 version.
$ variablePreviousUpdate = "my string"
$ variableList.append( ( "variablePreviousUpdate", "my string" ) )
# Variables from 0.12 version.
$ myNewVariable = 42
$ yetAnotherVariable = False
$ variableList.extend( [ ( "myNewVariable", 42 ), ( "yetAnotherVariable", False ) ] )
[...]
label after_load:
for atom in variableList:
if not hasattr( store, atom[0] ):
setattr( store, atom[0], atom[1] )
return
variableList
list with the name and default value of each new variables. Then in the after_load label, the list will be iterated, and if the variable don't exist, it will be automatically created with its default value.Note that there's a work around to get ride of the necessity to have the variables addition in both the start label and the after_load label. It's to put the creation in an independent label called from both labels.
Python:
label start:
call updateVariables
[...]
label after_load:
call updateVariables
[...]
label updateVariables:
if not hasattr( store, "myNewVariable" ):
$ myNewVariable = "Its default value"
[...]
updateVariables
will be used both when the game start, and when a saved game is loaded. The content of this label must only define default values of variables that weren't present in the very first release of the game, and must always firstly test if the variable already exist or not.Else, you'll either have a game that will start with higher values than expected (if you make error corrections in the updateVariables label by example, see below), or you'll constantly reset the variables (if you don't ensure first that the variable is effectively absent).
Thanks to tacito for the idea.
B - Variable alteration:
Now, like I said above, the main advantage of after_load is that it let you alter the value of existing variables present in the save file. This let you cover change in the content of the game already covered by the previous updates, as well as correct a bug present in a previous update.
1 - Changing a variable type:
Imagine that you have a variable named
encounteredMia
which is initially a boolean. Your intend at first was to have encounteredMia = False
if the player didn't encountered Mia, and obviously encounteredMia = True
after the encounter. But with the time going, you found it more useful to count the number of encounter instead of just having a flag saying, "hey, MC have encountered Mia". So, encounteredMia
past from a boolean to an integer... But it will apply only for everyone starting a new game. For a player using a saved game, it will still be a boolean.That's where the after_load label show it's full utility:
Python:
label after_load:
if encounteredMia is False:
$ encounteredMia = 0
elif encounteredMia is True:
$ encounteredMia = 1
return
encounteredMia
will automatically be changed into an integer if, and only if, it was a boolean in the saved file.Side note: In this particular case, a boolean to an integer, in fact it cause few problems. Python will understand what you try to do, and change "False" to "0", and "True" to "1". But it was the only example which crossed my mind.
2 - Increasing a variable value:
In the same way, imagine that you changed your game by adding 5 new
lovePoints
in the first part of it. Following this logic, you also raised some conditions in your game. Where 10 points were enough, the player now need 15 of them. Except that someone using a saved game will never have 15 points...Here again, the after_load label come to your rescue:
Python:
# Flag to not add the points more than once.
default patched0_12 = False
label start:
# It's a new game, there will be no need to artificially add points.
$ patched0_12 = True
[...]
label after_load:
if patched0_12 is False:
# Artificially add the 5 points for someone using an old save file.
$ lovePoints += 5
# Do not add the points twice.
$ patched0_12 = True
return
In the example I gave the full number of points, it's up to you to do this or assume that the player picked some wrong choice, and so add only half the number of points. It all depend of the way your game works. If the player need to always have the full number of points, be kind and give him all the added points.
3 - Decreasing a variable value:
The same let you decrease a variable value if you decide to remove some points. But in this case you must act with more caution, to not let the player be stuck because of your correction.
Increasing points is easy. You can easily assume the choice made by the player without risk to let him be stuck because of a lack of points. Like I implied above, add all the points if the player need to always be full points, else adding just half of the points is enough most of the time. But for a points reduction it's more difficult since you don't know if the player have the removed points or not, nor how many of them he have. In consequence, you need to do a little more works here.
The best case is when the player need to always have all the points, since you just have to take away the number of points you removed from the game. But in any other case you need to know your game and speculate in regard of the actual number of points. The average way to do is to split the number of points in five parts:
- If the player have just the number of points you removed, or less, you assume that he don't made the choice that now don't give points;
- If he have less than half the number of points, you take away a third of the number of points you removed;
- If he have around half the number of points, you take away half of the points;
- If he have more than half the number of points, you take away two third of the points;
- And finally if he have the full number of points, you take away all the points.
Python:
label after_load:
if points <= 6:
pass
elif points <= 12:
points -= 2
elif points <= 18:
points -= 3
elif points < 30:
points -= 4
elif:
points -= 6
return
4 - Adding a Boolean/flag with player's help:
Also note that after_load is a regular label. So you can put Ren'py statements in it. By doing this, you can also interact with the player.
Imagine, in your game the MC cross a real bad guy who talk badly to his girl. Then, the player have the possibility to punch him. At first, you saw this as a simple event, but later you thought that it can be a good idea to keep track of the player action. So, you added a
punched
boolean.But, how to deal with it in a saved file ? The default option is to force the variable to
True
in the after_load label. But it's not a fair move, the player can be peaceful and the story will change just because you assumed that he punched the guy. So, a better option is to ask the player what he did:
Python:
default patched0_12 = False
label start:
$ punched = False
$ patched0_12 = True
[...]
label after_load:
if patched0_12 is False:
"Did you punched the bad guys in the previous update ?"
menu:
"Of course!":
$ punched = True
"No, I'm a peaceful guy.":
$ punched = False
$ patched0_12 = True
return
There's an alternate way to do this, without the need of a
patchedXYZ
variable, but it can only works if your game is linear, or if at least if the different routes join regularly for the common part of the story.For this, you need to declare the variable by using the
default
statement, and giving it a neutral value ; generally the best value for this is None
, since its meaning is that there's no values. Then, on after_load label, or the first common label after the start of the new update, you'll ask the player for his previous choice... but only if the variable still have the neutral value.
Python:
default punched = None
[...]
label firstCommonLabelOfTheNewUpdate:
if punched is None:
"Did you punched the bad guys in the previous update ?"
menu:
"Of course!":
$ punched = True
"No, I'm a peaceful guy.":
$ punched = False
punched
is None
because the player use a save file where the variable didn't existed yet, or if it's None
because the player haven't yet passed the scene where he have to choose between punching the bad guy or not.5 - Automatically adding a Boolean/flag:
Thanks to Palanto for the suggestion.As you can imagine, having to ask the player for his previous choices can't really works if you suddenly add a bunch of variables. It's more an emergency solution than a suitable one. But if you planed it from the start of your game, you have a solution which can drastically reduce the number of questions, even if you add many variables. For this, you need to track the labels passed through by the player. It's done with the help of the
You must be registered to see the links
by adding this to your game's code:
Python:
init python:
def labelCB( lName, special=False ):
if renpy.get_filename_line()[0].startswith( 'renpy' ): return( lName, special )
store.labelPassedThrough.add( lName )
return( lName, special )
config.label_callback = labelCB
default labelPassedThrough = set( [] )
labelPassedThrough
set. Obviously, this do not works in a retroactive way. You can only know the label passed through while the callback was active. That's why this solution can only works if you planed it since the first version of your game.As for the use, imagine the following situation: In your game, there's two encounters with Mia, and the possibility to punch, or not, a bad guy. Like above, you want to now count the number of encounter with Mia, instead of the knowing that there was at least one, and you now want to keep track of the reaction of the player when facing the bad guy.
For the encounter with Mia it's easy, the two encounters have their own label ("miaFirstEncounter" and "miaSecondEncounter"). But for the bad guy it's more complicated. Like the MC is the hero, he knocked it down in one punch, and like he's also cool, he just continued walking after this. It mean that there isn't a particular label for you to know the choice made by the player. In the same time, it's an event which happen in an optional path, so you don't want to spoil the story by asking the player for a choice he perhaps don't had to make.
The solution looks like this:
Python:
init python:
[installation of the callback as above]
default patched0_12 = False
default punched = False
label start:
$ patched0_12 = True
[...]
label after_load:
if isinstance( encounteredMia, bool ):
$ encounteredMia = 0 # start with no encounter
if "miaFirstEncounter" in labelPassedThrough:
$ encounteredMia += 1
if "miaSecondEncounter" in labelPassedThrough:
$ encounteredMia += 1
if patched0_12 is False:
if "optionalLabelWithBadGuy" in labelPassedThrough:
"Did you punched the bad guys in the previous update ?"
menu:
"Of course !":
$ punched = True
"No, I'm a peaceful guy.":
$ punched = False
$ patched0_12 = True
return
encounteredMia
is automatic and will only happen if the variable is still a Boolean; so if the correction haven't been made yet. Firstly, the variable is transformed from a Boolean to an Integer. Then if the player passed through a label depicting one of the encounter, the number of encounter is increased by one. This let the player with 0 to 2 encounter with Mia, according to the reality of his actual play.As for the bad guy, it's a little more complicated. As previously, you'll ask the player for his choice, but this time you'll do it only if he played the optional event where the choice happen. Then, whatever the player had the possibility to punch him or not, you mark the game as patched, since it's effectively the case. The fact that the patch didn't had to be applied doesn't mean that it will be needed in the future. If the player finally follow this optional path, he will do it with your new code, which now directly change the value of
punched
.Note that this solution isn't limited to these two examples. It can also let you know if the player passed through a label where you added or removed points, and so if this addition/removal should apply to him or not.
6 - Correcting a variable related bug:
We are all human, and because of this we all make errors sometimes. When you're a game coder, these errors can have big consequences and make two players face different reaction of the game, while they still have done the same actions. This simply because one played a bugged update, will the other played after you corrected the bug. By chance, here again the after_load label can come to your rescue.
Imagine that you where really tired when writing your last update and that you made two errors. The first one was to write "numberofpoints" instead of "numberOfPoints". Python and Ren'py being case sensitive, those are two different variables. Your second error was to write
encounterMia -= 1
instead of encounterMia += 1
in a particular label.The consequences aren't small one. Not only anyone who've played this update before the correction will not benefit from the points added in it, but also instead of having encounterMia = 3, he will have encounterMia = 1. But don't worry, there's a solution:
Python:
init python:
[installation of the callback as above]
default patched0_12 = False
label start:
$ patched0_12 = True
[...]
label after_load:
if patched0_12 is False:
if "miaThirdEncounter" in labelPassedThrough:
$ encounteredMia += 2
$ numberOfPoints += numberofpoints
$ numberofpoints = 0
$ patched0_12 = True
return
numberofpoints
, and you add it to the correct variable numberOfPoints
. Then, obviously, you reset the value of the bogus variable ; just in case you make the same error again.Well, now you have no excuses. As long as you don't completely change the mechanism of your game, it can be save compatible most of the time. This whatever the variables you add in a new update, and whatever the way you changed the use of the existing variables. It can even be saved compatible if you add mandatory points/whatever in the parts already played, or if you finally decide to track some choice made by the player.
Even more importantly, you now have a way to correct many of the variable related bugs of an update without forcing the player to start a fresh new game.
Side Notes :
As discussed in the thread, the second cause of save incompatibility is when you play too much with the label's name. It's a little out of the subject, but still I decided to give it a quick cover to remove the blur.
Ren'py works with a strong mechanism of save loading, mostly relying on three things :
- The internal reference of the current line ;
- The label where this line is ;
- The content of the rollback stack.
As long as you didn't moved the label and it's content, or didn't deleted the .rpyc file, Ren'py will have almost no problems to load a save file. Remove lines in the label, Ren'py will load at the correct place. Add some lines, at worst Ren'py will load few lines before the one where the game was saved.
Problems can only come when you start to do more challenging operations ; like deleting the .rpyc file, moving the label, and its content, to another file, or renaming the label. But you must understand that the problems will effectively happen only if you do at least two of this at the same time. Therefore :
- You can safely delete the .rpyc file, as long as you don't rename a label in this file ;
- You can safely rename a label, as long as you don't delete the .rpyc file ;
- You can safely move a label, and its content, to another file, as long as you don't rename it.
It doesn't mean that you should do one of this things ; especially renaming a label, since you can forget to change a jump or a call statement to it. The fact that this will not break your game doesn't mean that it will not lead to inconveniences to the player.
But if really you haven't the choice and found yourself in the obligation to do so, at least you'll now know what is possible and what isn't.
As demonstration, I included a small "
You must be registered to see the links
" attached to this post. Don't hesitate to use it to have a better understanding of what happen and/or ensure that things still works the same with a new version of Ren'py.
Last edited: