Ren'Py Saving parts of objects in Renpy

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
Hi guys,

It's the RenPy Noob again.
I was wodering how Renpy is handeling the saving of objects.

So I have the following object.

Code:
init python:
    class Person:
        def __init__ (self, fname, lname, filter):
            self.fname = fname                      # First Name for Characters
            self.lname = lname                      # Last Name for Characters
            self.filter = filter                    # Filter Character from bar
I create an instance of that object like so

Code:
default Tjohn = Person("John","Doe",0)
The problem comes that I change the filter property during the game and it stays there as long as the game is open.
However, once I save the game, if I reload that game, I lose the filter and the filter is set to 1 again.

Is it possible to save this object so that Renpy remembers the filter setting?
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,609
2,256
There's something else going on here.

I wrote this code and it works fine (I tried reloading saves and quitting the game and loading again).

Python:
init python:

    class Person:
        def __init__ (self, fname, lname, filter):
            self.fname = fname                      # First Name for Characters
            self.lname = lname                      # Last Name for Characters
            self.filter = filter                    # Filter Character from bar


default Tjohn = Person("John", "Doe", 0)


label start:

    scene black with fade
    "Start:.... "

    "Start... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "Random Phrase 1"

    $ Tjohn.filter = 1
    "1... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "Random Phrase 2"

    "2... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "*** THE END ***"

    return

So if you're seeing the value resetting somehow, it's something in your code that you haven't already copy/pasted into this thread.

But to answer your basic question, objects/variables created while the game is running would normally be part of an overall store. object.

Imagine it works like this... changes to any variable within the store cause RenPy to take a copy of the whole store and add it to the rollback log. The rollback log is what is saved when you save (or autosave) the game. That isn't exactly the whole of it, but it's close enough for trying to understand it at this level.

Since your default Tjohn = Person("John", "Doe", 0) doesn't do anything odd... it too is part of store. (to test this, just open the developer console with {Shift+O} and type the command print store.Tjohn.fname any time you like). And since it is part of the main store, it's saved without the need to do anything else odd.

... unless I'm missing something fundamental. But since my example works... I'm assuming not.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,971
16,229
There's something else going on here.
Yeah. Since he create the object with default they will be saved, and save according to their values when the save is created.


And since it is part of the main store, it's saved without the need to do anything else odd.

... unless I'm missing something fundamental. But since my example works... I'm assuming not.
You missed the difference between define and default, and between the creation of an object in an init block and in a label. In both cases, only the second will be saved, but the two will still be part of the store object.
Code:
init python:
    initObject = renpy.python.RevertableObject()

default defaultObject = renpy.python.RevertableObject()
define defineObject = renpy.python.RevertableObject()

label start:
    $ labelObject = renpy.python.RevertableObject()
    call test( "defineObject" )
    call test( "defaultObject" )
    call test( "initObject" )
    call test( "labelObject" )
    "END"
    return

label test( objName ):
    if objName in renpy.python.store_dicts["store"].ever_been_changed:
        "The '[objName]' object will be saved."
    else:
        "The '[objName]' object will NOT be saved."
    return
But as I said above, in this particular case, the object will be saved. So, yes, it come from somewhere else. My guess is to look where the value is changed.
 
  • Like
Reactions: osanaiko

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
There's something else going on here.

I wrote this code and it works fine (I tried reloading saves and quitting the game and loading again).

Python:
init python:

    class Person:
        def __init__ (self, fname, lname, filter):
            self.fname = fname                      # First Name for Characters
            self.lname = lname                      # Last Name for Characters
            self.filter = filter                    # Filter Character from bar


default Tjohn = Person("John", "Doe", 0)


label start:

    scene black with fade
    "Start:.... "

    "Start... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "Random Phrase 1"

    $ Tjohn.filter = 1
    "1... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "Random Phrase 2"

    "2... Name: [Tjohn.fname] [Tjohn.lname]. Filter=[Tjohn.filter]"

    "*** THE END ***"

    return

So if you're seeing the value resetting somehow, it's something in your code that you haven't already copy/pasted into this thread.

But to answer your basic question, objects/variables created while the game is running would normally be part of an overall store. object.

Imagine it works like this... changes to any variable within the store cause RenPy to take a copy of the whole store and add it to the rollback log. The rollback log is what is saved when you save (or autosave) the game. That isn't exactly the whole of it, but it's close enough for trying to understand it at this level.

Since your default Tjohn = Person("John", "Doe", 0) doesn't do anything odd... it too is part of store. (to test this, just open the developer console with {Shift+O} and type the command print store.Tjohn.fname any time you like). And since it is part of the main store, it's saved without the need to do anything else odd.

... unless I'm missing something fundamental. But since my example works... I'm assuming not.
Thank you for the answer.

So I'm using the filter property to filter out certain character profiles or stats that the player might not want to see.
I know it works correctly, as the behaviour works 100% as expected. I also show that object property on my screen similar to console (Is still need to start using that trick :) )

In anycase...everything works 100%, but then if I save and load again, the value is back to it's old self. (0 in this instance)

I will keep looking.
 

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
Yeah. Since he create the object with default they will be saved, and save according to their values when the save is created.




You missed the difference between define and default, and between the creation of an object in an init block and in a label. In both cases, only the second will be saved, but the two will still be part of the store object.
Code:
init python:
    initObject = renpy.python.RevertableObject()

default defaultObject = renpy.python.RevertableObject()
define defineObject = renpy.python.RevertableObject()

label start:
    $ labelObject = renpy.python.RevertableObject()
    call test( "defineObject" )
    call test( "defaultObject" )
    call test( "initObject" )
    call test( "labelObject" )
    "END"
    return

label test( objName ):
    if objName in renpy.python.store_dicts["store"].ever_been_changed:
        "The '[objName]' object will be saved."
    else:
        "The '[objName]' object will NOT be saved."
    return
But as I said above, in this particular case, the object will be saved. So, yes, it come from somewhere else. My guess is to look where the value is changed.
So just a bit of clarity, the object was initially created with define, as I did not need to store any values of it, but now I need this one property. so I changed it to default.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,971
16,229
So just a bit of clarity, the object was initially created with define, as I did not need to store any values of it, but now I need this one property. so I changed it to default.
Hmm... I have doubt right now, but it's possible that it change something.

Do you have the problem (the value not saved) even when you start a new game ?
 

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
OK, I started a new game and now everything is working!!

Not sure what happened where.

Thanks for the help guys :)
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,609
2,256
Not sure what happened where.

I think your problem lies here...

[...] the object was initially created with define [...]

Best guess... the saved games you were trying to load were created before you swapped to using default.

Variables created using define are not saved since they are effectively static values (constants). Whilst their values can be changed, it is strongly recommended in the documentation that doing so can lead to very unpredictable results.

So if you load a saved game without the variable concerned being part of that save (in this case Tjohn), it will either continue to not exist (and probably cause a crash) or it will use a default value (in this case, "John", "Doe", 0). In your case, you would originally have had the define values to avoid a crash, but those defined values would continue to not be part of save files.

Beyond that, whilst you were changing the property of filter, that seems like it wasn't enough to trigger RenPy's need to flag the variable as needing saving... since the variable itself was Tjohn and not filter. A guess on my part, but an educated guess.

There are old ways and new ways to set up your variables. default is the new way... and ensures that variables always have values and those values are always saved. I could explain the old ways... but why bother? As long as you think "create changeable variables using default"... you'll be fine.

Whereas define is more for creating structures or constants.

Generally speaking... if you want it to be saved, use default, otherwise use define. If in doubt, use define - since it's "probably" the right answer (except when it isn't).
 
  • Like
Reactions: anne O'nymous

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
I think your problem lies here...




Best guess... the saved games you were trying to load were created before you swapped to using default.

Variables created using define are not saved since they are effectively static values (constants). Whilst their values can be changed, it is strongly recommended in the documentation that doing so can lead to very unpredictable results.

So if you load a saved game without the variable concerned being part of that save (in this case Tjohn), it will either continue to not exist (and probably cause a crash) or it will use a default value (in this case, "John", "Doe", 0). In your case, you would originally have had the define values to avoid a crash, but those defined values would continue to not be part of save files.

Beyond that, whilst you were changing the property of filter, that seems like it wasn't enough to trigger RenPy's need to flag the variable as needing saving... since the variable itself was Tjohn and not filter. A guess on my part, but an educated guess.

There are old ways and new ways to set up your variables. default is the new way... and ensures that variables always have values and those values are always saved. I could explain the old ways... but why bother? As long as you think "create changeable variables using default"... you'll be fine.

Whereas define is more for creating structures or constants.

Generally speaking... if you want it to be saved, use default, otherwise use define. If in doubt, use define - since it's "probably" the right answer (except when it isn't).
Thanks for your time and detailed answer...

The problem for me was that the define was perfect. (I might even have a post about that in this section)

But I decided on this version that I wanted to store a filter variable for each character. So instead of creating 50+ new variables, I thought I would just change the object definition to include that extra property. And then change the object init to default instead of define.... So I guess that is how that turned out...lol


Quick question...just out of interest.
If I were to default from the beginning and then add more properties to my object class, I assume that would cause some issues when loading an old save?
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,609
2,256
[...] and then add more properties to my object class, I assume that would cause some issues when loading an old save?
Yes.

In purely programming terms, you'd be loading an object that no longer matches it's definition.
There are ways to fix it, and it's a good sign that you already spotted the potential problem. But it's messy, especially if you alter the structure more than once during the life of your project. Better to plan ahead and avoid the headaches.

But also from a practical side... will your player's experience still make sense if you add something later? By far the easiest solution would be to reset the value to some default value when the code recognizes that the structure mismatches... but would that default value make sense in all circumstances? Linear VN's - it's probably going to be okay... an open world VN - probably not.
 
  • Like
Reactions: Alfius

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
Yes.

In purely programming terms, you'd be loading an object that no longer matches it's definition.
There are ways to fix it, and it's a good sign that you already spotted the potential problem. But it's messy, especially if you alter the structure more than once during the life of your project. Better to plan ahead and avoid the headaches.

But also from a practical side... will your player's experience still make sense if you add something later? By far the easiest solution would be to reset the value to some default value when the code recognizes that the structure mismatches... but would that default value make sense in all circumstances? Linear VN's - it's probably going to be okay... an open world VN - probably not.
Luckily, for me, this is not an essential variable. So I guess if I ever need to add another property, I will create new instances of the new object :D like TTjohn or something.
or like a secondary object

So Object Person and Object PersonEx
Then have Tjohn = Person and Tjohnx = PersonEx.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,609
2,256
Well, since you're already thinking about it.. consider this strategy:

Create a second object as an expanded version of the original, then use to copy the "old" data to the "new" object, whilst filing in the gaps as it goes along. Then destroy the "old" object to avoid future confusion. You'd need to update your code to use the new data rather than the original.

At the end of the day, use whatever solution works for you. I would do it that way, because I'd feel like having data in two places would be untidy. But that's purely personal preference.
 
  • Like
Reactions: Alfius

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,971
16,229
Best guess... the saved games you were trying to load were created before you swapped to using default.
I wasn't sure yesterday (it was late and I had a hard day), but yes, it's because of that.
In the save file, Ren'py already known about the variable, but known it as not savable. Therefore the change to default did nothing, keeping the variable as not savable.
But starting a new game reset what Ren'py know about the variables, making it effectively savable.
 
  • Like
Reactions: Alfius

Alfius

Engaged Member
Modder
Sep 30, 2017
2,348
4,920
I wasn't sure yesterday (it was late and I had a hard day), but yes, it's because of that.
In the save file, Ren'py already known about the variable, but known it as not savable. Therefore the change to default did nothing, keeping the variable as not savable.
But starting a new game reset what Ren'py know about the variables, making it effectively savable.
This makes a ton of sense
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,971
16,229
In purely programming terms, you'd be loading an object that no longer matches it's definition.
Technically speaking, no.
When loading a save file, the object is created using its actual definition (so including the new attributes), then the couples attribute/value present in the save file are assigned to the object. So it end being a hybrid between the new object and the saved one.

Therefore, the problems comes only if :
  • The default value for an added new attribute come from an argument of the __init__ method ;
    This attribute will loose its default value when an old save (previous to the change) will be loaded.
  • An attribute is removed ;
    The attribute will still exist, but it's not really a problem since it will never be used.
  • An attribute is renamed.
    The attribute will have its old name, and the new name will not exist.


Create a second object as an expanded version of the original, then use to copy the "old" data to the "new" object, whilst filing in the gaps as it goes along. Then destroy the "old" object to avoid future confusion.
Over complicated. He can just extend the object in "after_load" :
Python:
init python:
    class Person:
        def __init__ (self, fname, filter, added):
            self.fname = fname
            # This one is deleted
            # self.lname = lname
            # Changed name
            self.newFilter = filter
            # Version of the object
            self.version = 1
            # Added attributes with a 'dynamic' value
            self.thisIsNew = added
            # Added attribute with a default value
            self.newAttr = 0

label after_load:
    # For all the object of this class (I only know Tjohn)
    for obj in [ Tjohn, TotherObject ]:
        # The /version/ attribute do not exist, so it's an old version
        if not hasattr( obj, "version" ):
            #  Assign to the new attribute name the value of the old
            # attribute name.
            $ obj.newFilter = obj.filter
            # Delete the now useless attribute.
            $ del obj.filter
            # Delete the removed attribute.
            $ del obj.lname
            #  Always be paranoid, ensure that new attributes will exist.
            $ obj.newAttr = 0

# If a new change happen later, the object exist in its first updated version
#       elif obj.version == 1:
# Do here the changes for the second version

    #  Here come the new values defined at creation time. Since the value 
    # depend of the object, it can't be automatized.
    if not hasattr( Tjohn, "thisIsNew" ):
        $ Tjohn.thisIsNew = "the correct value"
    if not hasattr( TotherObject , "thisIsNew" ):
        $ TotherObject .thisIsNew = "another correct value"

    return
Since I'm at this, if you change an object from define to default, like here, you can force Ren'py to know it as savable :
Python:
label after_load:
    # Once again, all this object, but only their name this time
    for objName in [ "Tjohn", "TotherObject" ]:
        # If the object name isn't in the list of savable variables
        if not objName in renpy.python.store_dicts["store"].ever_been_changed:
            # just add it
            $ renpy.python.store_dicts["store"].ever_been_changed.add( objName )

    return