Ren'Py Question about save compatibility in Ren'Py

Adabelitoo

Well-Known Member
Jun 24, 2018
1,950
3,050
A few weeks ago I released the v0.1 of a game (Complex Society) and now I am working on things for the v0.2. I was adding a bunch of new variables and setting new flags when I realized that I never thought too much about save compatibility until that moment.

The first thing I did was searching about it in this forum and I found this thread about save compatibility but I'm afraid to admit that things got too techinical for me and after the fourth comment I accepted the fact that there wasn't enough coffee in the world to make me read and understand what were they talking about and how much of that thread is relevant to me.

So... if someone could write a Save Compatibility in Ren'Py for DUMMIES or give me a link to another place with info about it I would really appreciate it. Thanks for reading.
 
Last edited:
  • Like
Reactions: Abhai

Epadder

Programmer
Game Developer
Oct 25, 2016
568
1,064
I mean if that thread started making your brain spin... I don't know how much anne O'nymous 's guide here (https://f95zone.to/threads/how-to-variables-and-save-compatibility.16011/) will help, but there it is.

Otherwise you usually won't have problems with things moving forward unless you're changing what already exists.

If you add things it's not usually a problem, if you change things that were it can cause problems.

Removing/Renaming labels will usually lead to issues, although fairly recently there have been strides to help course correct those kind of errors...

Renpy Changelog said:
The new config.load_failed_label() specifies a label that is jumped to when a load fails because Ren'Py can no longer find the current statement. This makes it possible to a game to implement its own recovery mechanism.
If you start working with python classes and make significant changes to a class you can also break save games.

If you're keeping it fairly simple (working mainly with Interger/String/Boolean variables)... you should be okay. ;)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,234
I mean if that thread started making your brain spin... I don't know how much anne O'nymous 's guide here (https://f95zone.to/threads/how-to-variables-and-save-compatibility.16011/) will help, but there it is.
Normally it should be more easy to read. In the thread he mentioned, we were going really technical, talking about how and why this/that works or not. This while I tried to stay more at the surface of the problem in the How-To, talking about how things should be done, without necessarily explaining why unless it was really needed.
 

CarbonBlue

Developer of Unleashed
Game Developer
Oct 25, 2018
1,214
8,063
Not to hijack the thread or anything, but I had a similar question: I had no variables in 0.1 and in 0.2 I'll go back and add some. Is there a right way to invalidate saves, forcing people to go back and play 0.1?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,234
Not to hijack the thread or anything, but I had a similar question: I had no variables in 0.1 and in 0.2 I'll go back and add some. Is there a right way to invalidate saves, forcing people to go back and play 0.1?
Well, there's many ways. The easier is probably to benefit from the label. Something like this :

Python:
label start:
    # For once need to be declared inside the label.
    $ withVariables = True

label after_load:
    # If there's no attribute named "withVariables", then it's not a new game.
    if not hasattr( store, "withVariables" ):
        # Explain what happen.
        "Sorry, but you need to start a new game because this and that."
        # And force the game to go back to the main menu.
        $ renpy.full_restart()
But honestly I wouldn't advice this kind of behavior. It's better to explain what happened and ask nicely to the players to start a new game.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,611
2,258
Since the first thread you mentioned was mine, let me see if I can summarize the things that might matter to you.

First things first... When we talk about save compatibility, we're mainly talking about ensuring that the variables that 0.2+ need are available when you load a save file. It's oh so easy to go back and add a variable and then forget that a group of players out there have already played 0.1 and therefore have save files that don't include that new variable you added later.


default

Personally, I would recommend that all variables initially be defined using a default statement. By convention (not that everyone does the same), at the top of the script you are writing.

Any variables defined usingdefault ALWAYS exist, even if you added them later.
Imagine it like all those default statements get run before your code actually starts, no matter where they exist. You start a game normally... all those default values are used. If you load a save game that doesn't have those variables in... it still uses the default values. But if you load a save game where the value IS saved as part of the save file - that new value is used instead.

Variables defined using Python ($) only exist from the point that code is executed. So if you have added a $ myvar = True statement to version v0.1 code while working on v0.2 and a player loads a save game from the end of 0.1 in order to play v0.2 - if that save game is AFTER the place in the code where the myvar is set... it won't exist (and will probably throw an error if you reference it). default exists exactly to avoid shit like that.


after_load: hasattr() and $

Next.. as anne O'nymous already mentioned is after load:. Any time you load a save file, that bit of code gets run immediately after the save file is loaded (the clue is in the same). Combined with hasattr, it can be used to set values that previously didn't exist (though not if you're using default - since those variables will always exist).

So as an alternative, you could do something like:

Python:
label after_load:
    # If there's no attribute named "withVariables", then it's not a new game. So create it with a substitute value.
    if not hasattr( store, "withVariables" ):
        $ withVariables = 0
I'd suggest NOT using a value of 0 (zero) or False or whatever here, unless it makes sense.
Generally speaking, most players won't want to continue playing as if something didn't happen. So if you're adding something like love points retroactively, set it to the theoretical maximum... not zero. If you're checking if your player peeked on his sister in the shower, assume the player did and set the value to True rather than False.
Trust me, there will always be edge cases where whatever you do will cause problems... but code for easy stuff and let the edge cases go swivel.


after_load: and default

If you want to do this same sort of thing and still use default, then use null when you add a variable retrospectively, then use after load: to fix it for you.

Something like:

Python:
default love_points = 0
default sex_points = 0
default myvar = null

# [blah, blah, more code....]

label after_load:
    # If there's no attribute named "myvar", then it's not a new game. So create it with a substitute value.
    if myvar == null:
        $ myvar = 0

# [blah, blah, even more code....]
This assumes "myvar" has been added later. The default value is null (think of it as "no real value"). If you load a save game without myvar in it, the game will retain the default value of null... that check will be true and the "$" statement will replace the value with a new value (again... that makes sense for someone loading a save from the end of v0.1 or whatever).

Remember that you will still need another $ myvar = 0 (or similar) somewhere else in your code for players who start the game from scratch.
Which leads neatly into my last point...


a final note about default and after_load:

Despite default always setting you up a default value and after load: tweaking the values if a value didn't previously exist... it's still worth setting your variables to zero or False or whatever just before you use them for the first time.

I'll be honest, this is belt and braces stuff... to catch players doing odd things. (but partly to make you think about the potential too).

Imagine you add "love_points" to v0.1 code during v0.2 (or v0.3+... whatever). You add in all the places where it should add one when the player does something and eventually decide that the maximum a player could get normally during v0.1 would be 4.

So you code something like this:
Python:
default love_points = null

# [blah, blah, more code....]

label after_load:
    if love_points == null:
        $ love_points = 4

# [blah, blah, even more code....]

label start:

# [blah, blah, even more code....]
Okay. No problem. Your player loads their "end of v0.1" save game in order to continue to play your new v0.2 release. They start with 4 love points as if they previously maxed out everything and the game continues.

BUT...

What if another player started v0.1... put their name in... saved and then quit almost immediately?
They fire up 0.2. The after load: is invoked and the value is set to 4.
Then the player continues to play through all the code of v0.1 they didn't play the first time. By the time they reach the end of v0.1 - their love_points now add up to 8.

The answer is to still include a "$" statement to set the value to zero (or False, or whatever). Ideally, somewhere just before your first $ love_points += 1 or $ love_points -= 1 statement.

Python:
    $ love_points = 0

And finally...

I hope that covers the majority of issues you're likely to run into.
Honestly though... the real secret is to never add variables into previous releases retrospectively and always have a default statement per variable. Do both of those and 99% of your save compatibility issues will never arise.
 
Last edited:

Adabelitoo

Well-Known Member
Jun 24, 2018
1,950
3,050
Since the first thread you mentioned was mine, let me see if I can summarize the things that might matter to you.

First things first... When we talk about save compatibility, we're mainly talking about ensuring that the variables that 0.2+ need are available when you load a save file. It's oh so easy to go back and add a variable and then forget that a group of players out there have already played 0.1 and therefore have save files that don't include that new variable you added later.


default

Personally, I would recommend that all variables initially be defined using a default statement. By convention (not that everyone does the same), at the top of the script you are writing.

Any variables defined usingdefault ALWAYS exist, even if you added them later.
Imagine it like all those default statements get run before your code actually starts, no matter where they exist. You start a game normally... all those default values are used. If you load a save game that doesn't have those variables in... it still uses the default values. But if you load a save game where the value IS saved as part of the save file - that new value is used instead.

Variables defined using Python ($) only exist from the point that code is executed. So if you have added a $ myvar = True statement to version v0.1 code while working on v0.2 and a player loads a save game from the end of 0.1 in order to play v0.2 - if that save game is AFTER the place in the code where the myvar is set... it won't exist (and will probably throw an error if you reference it). default exists exactly to avoid shit like that.


after_load: hasattr() and $

Next.. as anne O'nymous already mentioned is after load:. Any time you load a save file, that bit of code gets run immediately after the save file is loaded (the clue is in the same). Combined with hasattr, it can be used to set values that previously didn't exist (though not if you're using default - since those variables will always exist).

So as an alternative, you could do something like:

Python:
label after_load:
    # If there's no attribute named "withVariables", then it's not a new game. So create it with a substitute value.
    if not hasattr( store, "withVariables" ):
        $ withVariables = 0
I'd suggest NOT using a value of 0 (zero) or False or whatever here, unless it makes sense.
Generally speaking, most players won't want to continue playing as if something didn't happen. So if you're adding something like love points retroactively, set it to the theoretical maximum... not zero. If you're checking if your player peeked on his sister in the shower, assume the player did and set the value to True rather than False.
Trust me, there will always be edge cases where whatever you do will cause problems... but code for easy stuff and let the edge cases go swivel.


after_load: and default

If you want to do this same sort of thing and still use default, then use null when you add a variable retrospectively, then use after load: to fix it for you.

Something like:

Python:
default love_points = 0
default sex_points = 0
default myvar = null

# [blah, blah, more code....]

label after_load:
    # If there's no attribute named "myvar", then it's not a new game. So create it with a substitute value.
    if myvar == null:
        $ myvar = 0

# [blah, blah, even more code....]
This assumes "myvar" has been added later. The default value is null (think of it as "no real value"). If you load a save game without myvar in it, the game will retain the default value of null... that check will be true and the "$" statement will replace the value with a new value (again... that makes sense for someone loading a save from the end of v0.1 or whatever).

Remember that you will still need another $ myvar = 0 (or similar) somewhere else in your code for players who start the game from scratch.
Which leads neatly into my last point...


a final note about default and after_load:

Despite default always setting you up a default value and after load: tweaking the values if a value didn't previously exist... it's still worth setting your variables to zero or False or whatever just before you use them for the first time.

I'll be honest, this is belt and braces stuff... to catch players doing odd things. (but partly to make you think about the potential too).

Imagine you add "love_points" to v0.1 code during v0.2 (or v0.3+... whatever). You add in all the places where it should add one when the player does something and eventually decide that the maximum a player could get normally during v0.1 would be 4.

So you code something like this:
Python:
default love_points = null

# [blah, blah, more code....]

label after_load:
    if love_points == null:
        $ love_points = 4

# [blah, blah, even more code....]

label start:

# [blah, blah, even more code....]
Okay. No problem. Your player loads their "end of v0.1" save game in order to continue to play your new v0.2 release. They start with 4 love points as if they previously maxed out everything and the game continues.

BUT...

What if another player started v0.1... put their name in... saved and then quit almost immediately?
They fire up 0.2. The after load: is invoked and the value is set to 4.
Then the player continues to play through all the code of v0.1 they didn't play the first time. By the time they reach the end of v0.1 - their love_points now add up to 8.

The answer is to still include a "$" statement to set the value to zero (or False, or whatever). Ideally, somewhere just before your first $ love_points += 1 or $ love_points -= 1 statement.

Python:
    $ love_points = 0

And finally...

I hope that covers the majority of issues you're likely to run into.
Honestly though... the real secret is to never add variables into previous releases retrospectively and always have a default statement per variable. Do both of those and 99% of your save compatibility issues will never arise.
Thank you so much dude! That was easy for me to understand. The only variables that I didn't use default was when I created the characters because the Ren'Py tutorial says to use define. Is it okay to do that? Or should I change it to default? Should I use default to add new characters from now on?
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,611
2,258
default and define are just fine.

Generally speaking, default is for any variable that may change value (and therefore needs to be included in a save file)...
... and define is for stuff that the player will never change. (images, transitions, constants, etc).

For example, you'd 99.9% of the time use define to create a character object.
But you'd probably use default to store the character's name (in a separate variable).

Something like:

Python:
define Al = Character("[al_s]", color="#663300", who_outlines=[ (1, "#000") ])
default al_s = "Alice"
You can change anything set up with define at any stage of development, because it is recreated each time the player starts a game and isn't expected to change during that playthrough.
 
  • Like
Reactions: Abhai

Adabelitoo

Well-Known Member
Jun 24, 2018
1,950
3,050
Lol, I wasn't expecting to see my own code as an example, that was weird but flattering at the same time. Once again thank you much.
 

GuyShy

New Member
Aug 1, 2019
7
12
You can also force a re-save, new variables will be saved as well
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,234
You can also force a re-save, new variables will be saved as well
Not really.
They will be saved only if they have been declared with default. Which is useless because in this case they'll be present in the game even if they weren't in the save file.
But for all the other case of figure, it will change absolutely nothing to the state of the game.

Use this piece of code:
Code:
label varState( what ):
    $ existState = "" if hasattr( store, what ) else " NOT"
    $ saveState = "" if what in renpy.python.store_dicts["store"].ever_been_changed else " NOT"
    "[what] do[existState] exist and will[saveState] be saved."
    return

label start:
    "Please, save here, then load your save file."
    "END"

label after_load:
    call varState( "initVar" )
    call varState( "defineVar" )
    call varState( "defaultVar" )
    call varState( "labelVar" )
    return
Save the "game", then add this into the code :
Code:
init python:
    initVar = 42

define defineVar = 42
default defaultVar = 42

label start:
    $ labelVar = 42
    [...]
Load the save file you've made before, and you'll see that only the "defaultVar" variable can be saved at this time, and that the "labelVar" don't even exist for the game.

So, forcing a re-save will change absolutely nothing to the state of the variables.