Do 'python early' blocks mess with the lifecycle of Ren'Py somehow? Persistent variables contained in custom classes are ignored

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,968
16,222
Since I'm using custom classes to store the scene item's information, I had to declare the class in an init python early block (to be able to persist the class).
Absolutely not.
The early init state is dedicated to user defined statements, in order for them to exist when Ren'py will parse the files, and it's its only role. It can happen that a class is defined at this moment, but only because it will be used by the said user defined statements, but it's the only reason why the class is defined in an init early block.


Python:
    python:
        import math

        GALLERY_SIZE = len(persistent.scene_list)
        GALLERY_COLUMNS = 3
        GALLERY_ROWS = int(math.ceil((float(GALLERY_SIZE) / GALLERY_COLUMNS)))
        EMPTY_COUNT = GALLERY_ROWS * GALLERY_COLUMNS - GALLERY_SIZE
It's not the place for a module importation. Importation should be global, not local, especially not local to a screen since they have their own context ; I'm not even sure that because of this question of context, it don't have some influence.
And also what's the interest to have something like int(math.ceil(...)), since math.ceil is specifically made to return an integer ?


Python:
                                            add "multi_part_unlocked"
[...]
                                            add "multi_part_locked"
What are you trying to achieve here ? The add screen statement is to add a displayable, either an image or a user defined displayable.
If you want to add an image, then the extension is missing, if you want to add a displayable, then it shouldn't be a string, and if you wanted to do something else, then it's absolutely not how you should do it.


I'd appreciate any help which would bring me closer to figuring out this issue.
Well, firstly simplify your code.

By example, you don't need to have things like :
Code:
           self.unlocked_idle_image = unlocked_idle_image
            self.unlocked_hover_image = unlocked_hover_image
            self.locked_image = locked_image
especially when the value are "gallery_item_sicily_idle", "gallery_item_sicily_hover", "gallery_item_locked"
According to its name, locked_image is a constant, you don't need to store it in each object. As for the two firsts, it can be "gallery_item_sicily_%s", that would permit to replace :
Code:
                               idle scene_element.unlocked_idle_image
                                hover scene_element.unlocked_hover_image
by :
Code:
                               auto scene_element.unlocked_image
And obviously, solve this add thing before everything.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,968
16,222
I'm sorry but you are wrong here. If you want to persist data not native to Python or Ren'Py, you have to declare that class in an early Python block.
My bad ; forgot that persistent are loaded by anticipation due to its nature.


I'm not sure I follow you here. Isn't it supposed to be bound to the python block, and not the screen?
Module importation are always global, so available whatever in the code once done ; unless the importation is made inside a function or class. By example, renpy/common/00achievement.rpy import a module under the name "steam" into the achievement store. You can address it, by example from the console, through achievement.steam. This despite the fact that you obviously aren't anymore in the init block that imported it, and that this importation is made from an alternate store. Therefore, just have an init block at the top of the file to import the module once for all.

It also imply that, no, the importation isn't bound to the Python block. But if it was, then it would be bound to the screen, since the said Python block is itself bound to this screen. And it's where it can, possibly, generate some problems since screen have their own context.
Normally everything should be alright, due to the global nature of importation, but context are really sensible, so I don't guaranty it.


My understanding of blocks in Ren'Py might be wrong, but when the game compiles, won't all python blocks (unless given an order prefix) run asynchronously (when needed in this specific scenario)?
They will, but it don't mean that they are isolated piece of code.
Ren'py is nothing more than a big and complex Python script. Anything, whatever it's a statement, an inline Python line, or a Python block, is just one piece of this big entity. Whatever happen in a Python block will have an impact on the rest of the script ; else the said blocks would be totally useless.


The behaviour change was made in Python 3.0. [...]
The fuck :/ It's the first time I mess between the two branches :( My bad again.


I'm trying to add X number of locked/unlocked icons below the SceneItem's thumbnail, in a hbox. I thought that I'd simply use the add statement since the image is not interactive.
You can, but it's really unusual to address the image entity in place of the file itself ; mostly because added images tend to be generic and so can have conflictual names.


My issue doesn't seem to be with displaying the icons but rather the code itself [...]
I know, but by itself your code is correct, so I focused on the wrong or unusual parts of the code, since they could have been the source of Ren'py confusion.


However, I'm not closer to my solution :(
What happen when you proceed the elements outside of a screen ? Something like :
Python:
label start:
    while True:
        call whatever
        "next loop"

label whatever:
    # There's no /for/ statement outside of screens :(
    python:
        for scene_element in persistent.scene_list:
            # If the list is empty, the loop will just be skipped.
            for persistent_variable in scene_element.multi_part_unlocks:
                if persistent_variable is True:
                     narrator( "{} value is {}".format( scene_element.item_name, persistent_variable ) )
                else:
                    narrator( "{} value is {}".format( scene_element.item_name, persistent_variable ) )
    return
Are the results constant, or do you also have some inconstancy like for the screen ?


My hunch is that something is either wrong with the for loop which checks the multi_part_unlocks list inside a SceneItem, or something completely different.
It looks more like a problem of context. It's totally not natural that something like your
Code:
                                       if persistent_variable == True:
                                            $ print("Adding UNLOCKED button with scene element ", scene_element.item_name, "boolean value is", (persistent_variable))
have a different value for persistent_variable in the if statement then right after in the print function.
More than "not natural", it should be totally impossible.

Therefore, my guess is that there's something that mess with the context, processing the statement in a context, and the function in a different one ; with the variable value being different in each context.

So, firstly you should eliminate the problem of the console. Normally the print function should be proceeded in the actual context, but the console having its own, perhaps that...
Replace those two print by text ( "Adding UNLOCKED button with scene element {} boolean value is {}".format( scene_element.item_name, persistent_variable ) )
Do you still have the inconstancy ?

If yes, then do not put your code as inclusion to the use statement. Or better, use the "navigation" screen in place of "game_menu" one ; still without including your code as block for the statement. Something like :
Code:
[...]
   use navigation

   frame:
        [probably need a positioning here]
        style_prefix "underconst"

        $ print ("-----------------------")
        $ print("Adding scene gallery")
        $ print ("-----------------------")
        vbox:
            label "Replay your favourite moments"
[...]
Variables sharing between an use[icode]d screen and its calling screen are really capricious. Therefore it's not impossible that there's effectively a double context because of this.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,968
16,222
Ren'Py seems to store their values, not their references.
And then I also missed the end of your assignation line, [persistent.fiona_other_stuff, persistent.fiona_another_stuff] which is the problem ; clearly it wasn't the good day for me to answer, sorry.



Ren'Py is weird. I don't see a reason why it should copy the value instead of holding a reference to the object, but oh well.
Still there's a reason, and a particularly good one:
Due to the immutable nature of variables in Python, the notion of reference do not exist in this language ; at least not like you imagine it. Everything is a value, or more precisely an object representing this value ; and I mean really everything, even strings, numbers and boolean are an object.
Because of that, if effectively two variables can point to the same object, because you assigned a variable to another one, change the value of one, and they'll stop to be linked.

[in the console]
Code:
a = "string"
b = a
id( a ) == id( b )
b = "different"
id( a ) == id( b )