Ren'Py Tutorial [How-To] Variables and save compatibility...

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
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:

1 - In an init block:

Python:
init python:
    myNewVariable = False
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.


2 - By using the statement:

Python:
default myNewVariable = 1

label start:
    [...]
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.


3 - At the start of the update:

Python:
label day12:
    $ day12_firstVariable = False
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.


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
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 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
Just update the 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"
    [...]
But always keep in mind that 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
When the player will load a saved file, 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
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.


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.
In practice, the result looks like this for 6 points removed over initially 30 possible 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
new content :
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
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 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 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( [] )
Now, if during his current play the player passed through a given label, you'll find the name of this label into the 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
The correction of 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
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 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 " " 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:

Palanto

Active Member
Game Developer
Oct 4, 2017
964
1,841
Awesome guide anne O'nymous. But that doesn't really mean it's always as easy to do as you make it out to be :) Sure one can ALWAYS make it savegame compatible, but sometimes you just did a stupid little mistake which fucked up ALL variables (like in my case, I forgot the return at the end of my label after_load and ended up going through all my functions and so on X_x ) so of course there is a way to make the savegames still compatible, but by asking all the choices the game had over again the players could just have played the game from the beginning again ;)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
Awesome guide anne O'nymous. But that doesn't really mean it's always as easy to do as you make it out to be :)
Shhh, they don't know it ;)
More seriously, yes, there's, alas, cases where it's not possible, or at least not that easy.


[...] but by asking all the choices the game had over again the players could just have played the game from the beginning again ;)
Lately I played a game like that ; I don't remember the name, perhaps it's a chance. Suddenly the author decided to track all the player's major decisions, adding more than 20 Boolean in one shoot. It's obvious that, in this case, it's better to restart a fresh game ; especially since the said Boolean values can have had an effect before the point where the game is saved.
But there's also many case where it's just a Boolean which is added, mostly because it was forgot at first. And this part of the solution obviously pointed them more than the full change case.

In fact, I wrote it after have found yet another author thinking that variable addition and save compatibility where... er, incompatible. And it triggered something in me.
There's obviously the case of those authors, but also those who make a single mistake in their code, or restraint themselves to add a small feature because it will add a variable. My intent is more to target them than preach for an utopian "all update are save compatible" world ; mostly because I know that it's just that, an utopia.
I know, to have helped some and read others, that there's authors fearing the kind of mistake that we all make (whatever it's a typo in the variable name or a "+" instead of a "-", by example). Either they keep the error and try to deal with the consequences, or they correct it and any player using a save from this update will have a bogus game. Now, they know that there's perhaps a solution. Like by example, for the case of a typo in the name, using after_load to report the value from the bogus variable to the legit one.
It can't help them cover all the errors, but if it can help them cover the small ones, at least when they are related to variables, it's a benefit for both them and the players.
 
  • Like
Reactions: botc76 and Palanto

Palanto

Active Member
Game Developer
Oct 4, 2017
964
1,841
Lately I played a game like that ; I don't remember the name, perhaps it's a chance. Suddenly the author decided to track all the player's major decisions, adding more than 20 Boolean in one shoot. It's obvious that, in this case, it's better to restart a fresh game ; especially since the said Boolean values can have had an effect before the point where the game is saved.
But there's also many case where it's just a Boolean which is added, mostly because it was forgot at first. And this part of the solution obviously pointed them more than the full change case.

In fact, I wrote it after have found yet another author thinking that variable addition and save compatibility where... er, incompatible. And it triggered something in me.
There's obviously the case of those authors, but also those who make a single mistake in their code, or restraint themselves to add a small feature because it will add a variable. My intent is more to target them than preach for an utopian "all update are save compatible" world ; mostly because I know that it's just that, an utopia.
I know, to have helped some and read others, that there's authors fearing the kind of mistake that we all make (whatever it's a typo in the variable name or a "+" instead of a "-", by example). Either they keep the error and try to deal with the consequences, or they correct it and any player using a save from this update will have a bogus game. Now, they know that there's perhaps a solution. Like by example, for the case of a typo in the name, using after_load to report the value from the bogus variable to the legit one.
It can't help them cover all the errors, but if it can help them cover the small ones, at least when they are related to variables, it's a benefit for both them and the players.
Well I did a patch this version which actually really asked the players for all the major choices and what they chose. Because of my mistake, so yeah it's possible but it's a lot of extra work that's really not necessary for an in dev game :) But yeah didn't want to hear the complaints again about the horror that they have to start a game all over again.... ;)

One thing which might be good to mention in your guide here would be the config.label_callback, which can be a major help later if you have to add something in a prior versions code. That way the dev can check if the player actually saw that label in the current playthrough or not, and if he did he can ask the player what he chose or automatically change points corresponding to the label...
In your example it would be:

did he see: label punchedTheGuy ? Yes? $ punchedTheGuy = True ;)
 
  • Like
Reactions: anne O'nymous

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
One thing which might be good to mention in your guide here would be the label_callback, which can be a major help later if you have to add something in a prior versions code. That way the dev can check if the player actually saw that label in the current playthrough or not, and if he did he can ask the player what he chose or automatically change points corresponding to the label...
Didn't thought about it.
But isn't it too much works for a "just in case" situation ? Because the callback must be in place since the start, and will really be used only if the author have to add/change something. Will in the same time being a list/set that will grow quickly since Ren'py's core have a lot of labels ; by example you pass through around 10 labels before hitting the start label, and just saving the game add at least 5 others.

This said, the callback isn't mandatory since Ren'py come with this feature already, renpy.seen_label (which apparently isn't in the official doc). The problem being that it's a persistent variable, so the content is not limited to this play. But, well, there's a way to solve this problem, just reset the value in the start label ; which have a price... no more skipping for what was seen in the previous play, players will have to use the "auto" feature instead.

Code:
label start:
    $ renpy.game.persistent._seen_ever.clear()
    [...]

label after_load:
    if renpy.seen_label( "punchedTheGuy" ):
        $ punchedTheGuy = True
    return
Well, now time to add this in OP. But probably not this night.
 
  • Like
Reactions: Palanto

Palanto

Active Member
Game Developer
Oct 4, 2017
964
1,841
Well and actually that's the big problem, the seen_label is persistent and used for something else. But those labels you said are just added once to the list with each playthrough, so even if you save multiple times it doesn't add the labels more than once (at least it shouldn't X_x). Yes it's kinda bad that it has to be inside the game from the first version or the players would have to play from the beginning again (just one of the many special cases where it would be unavoidable)...

Found a snippet, didn't "try" it myself yet, always wanted to test it but didn't think I'd need it for now since Wicked Choices Book I is almost finished (only one more update)

written by Remix on the ren'py discord server:
Code:
default visited_labels = []
init python:
    def label_callback( label, norm ):
        global visited_labels
        if not label in visited_labels:
            visited_labels.append( label )
    config.label_callback = label_callback
"fly-typed so might need tweaks ... note it does not test if they read the text, just when they arrived at label"
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
"fly-typed so might need tweaks ... note it does not test if they read the text, just when they arrived at label"
Quick tweak :
Code:
init python:

    def label_callback( lName, special=False ):
        # No need for Ren'py's internal labels
        if renpy.get_filename_line()[0].startswith( 'renpy' ): return( lName, special )
        store.labelPassedThrough.add( lName )
        # be compliant with modders please :D
        return( lName, special )

default labelPassedThrough = set( [] )
Writing it, it's in fact not such a thing and don't lead to as much possible issue that my memory tried to say. I will definitively add it, with the "_seen_ever" as a possible workaround in case of urgent need.
 
  • Like
Reactions: Palanto

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
OP updated.

I changed a little the structure, splitting the document in two parts (addition/alteration). By consequence I also changed the title since it don't anymore regard only variable addition.
I also added a part dedicated to points removal, one to automatic correction as suggested by @Palanto , and one dedicated to bug correction. The last one is a little useless, since it's mostly a "say it again" thing. But like not all the authors have good knowledge in coding, it seem useful for me ; it explicitly tell them that, yes it's possible to also do this.


I think that now most of the subject is covered.
 
  • Like
Reactions: Palanto

Palanto

Active Member
Game Developer
Oct 4, 2017
964
1,841
Awesome, now it's perfect and probably covers all possible cases (at least I can't think of any) :D
 
  • Like
Reactions: anne O'nymous

Beuc

New Member
Nov 11, 2018
13
7
Wrt the config.label_callback trick: isn't that recreating renpy.seen_label()?
 

Beuc

New Member
Nov 11, 2018
13
7
I'd add that removing and recreating a .rpyc file will corrupt most savegames that were in that part of the Story (unless a label was nearby enough).
This can be a problem if multiple devs work on the same .rpy files but do not share their .rpyc.
Maybe it's best to first grab the .rpyc file from the last release and run "force recompile" before a bugfix release. Just in case :)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
Wrt the config.label_callback trick: isn't that recreating renpy.seen_label()?
No. It's discussed above on the thread why and how they are different.


I'd add that removing and recreating a .rpyc file will corrupt most savegames that were in that part of the Story (unless a label was nearby enough).
Wrong. "compilation" will have no influences, and not only because it's a consistent process. It's also, among many reason, because the label is passed to the callback as plain name. Not only it will works fine whatever how many time the source have been "compiled", but it will even works fine if you move the label from "script.rpy" to "my_scripts/splited/character/mc/parts/01/main.rpy".

And I know this for sure since the config.label_callback is part of the trick behind the unrandomization in my mod for the game Corruption. And since I don't just need to know if the player passed through a given label, but to know what is effectively the label at a given instant, it's a more advanced and sensible code that the one I gave here. I mean, it need to return a reliable value even if, between the moment where Ren'py effectively entered the label and now, the game passed through an infinite number of called label.
Therefore, it mean that since now ten months, many people used it, on many computers and OSes, and it passed through many updates of the game. If what you say was true, it wouldn't have survived the first update.

Now, I can have missed something, don't hesitate to explain your thoughts.
 

Beuc

New Member
Nov 11, 2018
13
7
Thanks, it may be worth dissing 'seen_label' in the OP :)
(also the official doc is here )

About removing and recreating (not recompiling, I mean deleting the .rpyc and then recompiling it), this is a separate topic and a more general issue (not related to variables, just to savegame compatibility).
Just try: Ren'Py will immediately say it can't find label if you try and load a savegame from before delete/recompile. This also creates new texts identifiers and your "seen texts" gets basically reset for that file. You need to have Ren'Py update the original .rpyc continuously to avoid that issue :/
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
It may be worth mentioning seen_label in the OP.
No, because it's a persistent value... It will totally break the game if the player restart it at one time. This because the bug fixing code will falsely think that the player was passed through a given label.
Have you even read the eight other messages of this thread ?


About removing and recreating (not recompiling, I mean deleting the .rpyc and then recompiling it), this is a separate topic and a more general issue.
You know (well in fact apparently you don't) that "recompiling" mean that, between Ren'py and the OS, the .rpyc file will be deleted and then "recompiled" ? And I insit on the quote marks, since it's in fact more a pickled version of the AST for this file, than anything else.


Just try: Ren'Py will immediately say it can't find label if you try and load a savegame from before delete/recompile. You need to have Ren'Py update the original .rpyc continuously to avoid that issue.
I'll be honest, I don't know what to answer to this since it's totally WTF (sorry). Ren'py can not say that it don't find the label... and there's a reason really simple for this : Not a single time during the whole process, Ren'py is asked to find a label.

The callback will be called when Ren'py execute a Label node ; it's the last line of the execute method of the class. This mean that either it find the label, or the callback is not concerned yet.

What's passed to the callback is the name attribute of the said Label node. This mean the string found by the parser between the "label" starting the statement's line, and the ":" ending it. Here again, Ren'py can't say that he don't found the label, because it's still in the Label node. Whatever how many time you'll "compile" and "recompile" the source, what's wrote on the source will not change by miracle and the parser will not change its behavior. This mean that the value of the name attribute will not change either.

Passed this point, Ren'py isn't concerned anymore. What was a label name is now a string stored in a set. So, either "this" in setName will return True, or it will return False. None of these cases will make Ren'py complain, and none of these cases will be impacted by the fact that the source have been "compiled" and "recompiled" many time ; not even by the fact that they will be deleted then "recompiled".

So, like I said, when you use the config.label_callback, there isn't a single case where Ren'py can say that he didn't found the label. And this is confirmed by the fact that the said callback is mentioned (and used) many times in the official forum, since more than five years now, as an effective way to keep track of the passed labels and perform some other tricks.
What can happen, is that you don't address the label by its name, and search it in the internal equivalence table of Ren'py ; so in the renpy.game.script.namemap dict. Here, yes, the keys are, perhaps more or less, dependent of the compilation. But here only and yet each labels have an entry with its plain name :
Code:
"start" in renpy.game.script.namemap
will return True. And
Code:
renpy.game.script.namemap["start"].name
will return what's passed to the callback, "start".
But... well, whatever how many time you'll "compile", deleted, then "recompile" the source, the "start" key of the namemap will always be "start", and the "name" attribute of the object stored by this key will always be the string "start".
Ren'py can also complain if, instead of addressing the label by its name, you try to address it by its object. But it will not be because of a "recompilation", it will be because the object is recreated each time the "compiled" code is unpickled ; and so each time a different object.
There's also the fact that config.label_callback, like all the attributes of the config object, is not saved. But this will interfere only if you use an object for the callback and have the set as attribute of it. None are the case here. Anyway, it will have nothing to do with the source "compilation".

So, I don't know how you tried it, but either you did something really strange, did it wrong, you found a really insidious bug that no one encountered since the creation of this callback more than five years ago, or you failed to explained the problem.
And yes, I tried before answering you. Hell, I even installed the SDK on another computer with another OS, moved the .rpy file inside a new project, launched this project to force the compilation, then moved back the .rpyc files on my main computer, after deletion of the actual .rpyc files, launched the game and loaded a save with three instances of the callback hooked one to the other.
I'm still waiting for Ren'py to tell me that he can't found a label.
 

Beuc

New Member
Nov 11, 2018
13
7
Pal, I don't know why you're using this tone.

About seen_label, I said it was worth mentionning it to explain why this isn't a good solution.

For the "delete/recompile" point, as much as I don't like using arguments of authority, you're contradicting what renpytom answered me a couple days ago.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
About seen_label, I said it was worth mentionning it to explain why this isn't a good solution.
And it's already said in the thread, twice now, so it's already explained.
An how-to should be wrote to explain things in a less confusing way possible, not the opposite. So, no need to talk about what will not works. Especially when in this particular case, there's tons of false good solutions that will not works and just listing them briefly would be longer than the actual text.


For the "delete/recompile" point, as much as I don't like using arguments of authority, you're contradicting what renpytom answered me a couple days ago.
Alright, so the problem is that you don't really understood what you are talking about.
When PyTom talked about the broke save, he was referring to what you call "seen_text" and is the persistent._seen_ever dict, hidden behind both the renpy.is_seen and renpy.game.context().seen_current[/] functions. But this has nothing to do with what we are talking about here.

The _seen_ever persistent dict will effectively "broke" the saves because, like for the equivalence table I talked about above, it's directly related to the actual values of the .rpyc file.
In the console :
Code:
persistent._seen_ever.items()[:10]
[code]
Will return thinks like :
[quote]( ("full/path/to/file/file.rpy",124234,123), True )[/quote]
This while :
[code]
renpy.game.script.namemap.items()[:10]
will return the same kind of keys, but this time with an AST object as value.

But like I said, explained, and demonstrated, in my previous answer, the equivalence table is not used by any code related to config.label_callback ; not a single time during the whole process. What is used here are the plain text name of the labels, and they are totally independents of the "compiled" version of the source.

Side note: When PyTom talked about "broke" saves, it should have used quotation marks, because the saves aren't effectively broke. It's just the particular feature you were talking about that will not works anymore.
Everyday, just here, hundreds of people deal with "deleted .rpyc files" and still continue to use their saved games without problems. More than half of the mods for Ren'py games you'll find here need that you replace the original sources, and so delete, in a way or another, the original ".rpyc" files and replace it with a new one. This since more that two years now.
(Too) many game's authors put all their code in the sole "script.rpy" file, which mean that each update use a new ".rpyc" file. If it was effectively breaking the saves, and not only the "skip text seen before" feature, it would be known since age. In fact, it would be known since the very first release of Ren'py, because it would imply that, anytime they test what they added to their game, devs would face broke saves.

So, no, deleting/"recompiling" an ".rpyc" fille will not break the saves. Ren'py even include a inline configuration option to force this "recompilation" every time the game is launched, this even if the source haven't changed.
arguments.py/ArgumentParser class:
Code:
        self.add_argument(
            "--compile", action='store_true', dest='compile',
            help='Forces all .rpy scripts to be recompiled before proceeding.')
It wouldn't if it was effectively breaking the saves.
 

Beuc

New Member
Nov 11, 2018
13
7
I tested, and this breaks the savegame (renpy exception, unless an explicit label is in the rollback buffer, in which case rollback there).
You missed that "force recompilation" updates a .rpyc file; while delete/recompile recreates it from scratch (with different internal identifiers).
Confirmed here as well:
I'm not going to further argue, .rpyc files are just not meant to be deleted. Break your games if that's what you want ;)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
I tested, and this breaks the savegame (renpy exception, unless an explicit label is in the rollback buffer, in which case rollback there).
Alright, so provide the code you used... and, obviously, point the part where it's because there's a string stored in a set by the config.label_callback that you have an exception.
The obvious part is mandatory. I mean, when you'll try to find a working (well, in fact crashing) code, perhaps that you'll finally understood what I said from the start. Not a single time the config.label_callback will be impacted by what you do with your labels between two updates of the code.


You missed that "force recompilation" updates a .rpyc file; while delete/recompile recreates it from scratch (with different internal identifiers).
Have you read a single line of what I answered to you ? Or is it that you don't understood what I said ? It's a possibility, English is not my natural language and perhaps also not yours, so misunderstanding can happen.
Still, since my very first answer to you, I explicitly said that the internal identifiers will be different. And I also explained that it doesn't matter, because they aren't used a single time when you use config.label_callback.


Confirmed here as well:
At no times the post of PyTom you linked is related to what you affirm (that using "config.label_callback" will break the save compatibility).
Yes, if you play too much with your labels, Ren'py can (not "will") have trouble (not "impossibility") to load your saves. And so ? It's common knowledge ; just on this forum, you can find hundreds of comments talking about those issues and/or how to works with them, coming from tens of different users, me included.

But, I'll say it a third in the same post, perhaps that you'll read at least one, it's absolutely not related to the config.label_callback callback. At no time it will be impacted by what you do with your labels between two updates, nor by the fact that you'll deleted the ".rpyc" files.
And there's absolutely nothing in the post you linked that goes against this absolute truth.


Break your games if that's what you want ;)
You know, I really start to wonder what's the problem. Have you just quickly wrote random things to reach the post limit before you can put an ads for your Patreon page, then don't even bother to read what you answer to ? Or are you really not understanding what you talk about ?

I mean, you talk about an effective problem, knew since a long time by the majority of Ren'py users, that is absolutely not related to the subject.
What you do is like saying that you risk to crash your car if you drive while it rain. This in answer to my affirmation that, in case of rain, using an umbrella can help you to not be wet. Yes, we both talk about something related to rain, but that's the only relation. In no case your affirmation contradict mine, for the simple reason that you talk about something completely different. Therefore, you can provide all the studies about "driving in the rain", that you want, it will change nothing ; Using an umbrella is still a good way to not be wet when it's raining.

And if you still can't understand that, well what can I say that'll not be harsh ? Er... well, nothing.
 
  • Like
Reactions: botc76

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,846
15,966
To clarify the situation, I added an addendum to the How-to. I also included a script to demonstrate how effectively Ren'py react in case of deletion of the .rpyc file and/or operations on the labels (moving, renaming them). This will perhaps remove some fears, since it works as expected and don't effectively complain.
But I remember that, in any case, one of these operations will affect the use of the config.label_callback callback. It's called before the first line of the label is effectively played by Ren'py and, once called, it don't care anymore of what will happen to the label.
 
  • Like
Reactions: botc76

Beuc

New Member
Nov 11, 2018
13
7
it may be worth dissing 'seen_label' in the OP[...]
About removing and recreating (not recompiling, I mean deleting the .rpyc and then recompiling it), this is a separate topic and a more general issue (not related to variables, just to savegame compatibility).
perhaps that you'll read at least one, it's absolutely not related to the config.label_callback callback.
Yeah that's what I had written. You can stop trying to convert a believer :)
It seems you (ironically) misread me from the start.

I tested, and this breaks the savegame (renpy exception, unless an explicit label is in the rollback buffer, in which case rollback there).
So to reproduce the exception, one can add config.rollback_length+1 dialogue lines with no intermediate label in your sample script, and save at the last dialogue line.
(or even fewer if the player rolled back a few times before saving.)
In addition, whether the savegame is broken or salvageable, unseen texts for that script are reset, annoying when you replay the game.
People, don't delete .rpyc files.

(Note: I don't condone under-the-belt attacks but yours turned entertaining - keep going :)
Feels like we're talking about imminent XK-class end-of-the-world scenario or something.)