Reloading the game resets my python Classes

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
Lacha
Newbie
Posts: 12
Joined: Wed Mar 06, 2024 12:23 pm
Contact:

Reloading the game resets my python Classes

#1 Post by Lacha »

Hello everybody.

I am new to this forum. That is mainly because in the past I didn't encounter a problem I wasn't able to solve by myself. I am writing my first visual novel with renpy, but not the first program.

I am encountering a problem lately and I was able to figure out why this happens, but not how I get around it.
The problem is: Renpy resets my classes to initialization values when I reload the game. In most cases this is not too bad. But there is one case where it completely destroys my gameflow.
First off I started with a handful of characters and had each stat of the characters in a single variable. But as I intruduced more characters to my game it quickly got out of hand and so I decided to create a character-class named Person.
It looks like this:

Code: Select all

init -1 python:
    characters = []
    class Person:
        def __init__(self, varname, name, age, maxlove, maxlust, upkeep):
            global characters
            self.varname = varname
            self.name = name
            self.age = age
            self.upkeep = upkeep
            self.love = 0
            self.lust = 0
            self.maxlove = maxlove
            self.maxlust = maxlust
            self.active = False
            self.tapsinname = name.replace("th", "t").replace("Th", "T").replace("TH","T")
            self.location = "nowhere"
            self.state = 1 
            if not self in characters:
                characters.append(self)
            if self.name[-1] == "s":
                self.refname = self.name
            else:
                self.refname = name+"s"
            self.ref = self
        def activate(self):
            self.active = True
            shuffleOccupancy()
        def pointsadd(self, stat, points, waypoint):
            if points >= 0:
                prefix = "+"
            else:
                prefix = ""
            if waypoint in waypoints:
                if waypoints[waypoint]['stat'] == "lust":
                    waypoints[waypoint]['char'].lust += waypoints[waypoint]['points']*(0-1)
                if waypoints[waypoint]['stat'] == "love":
                    waypoints[waypoint]['char'].love += waypoints[waypoint]['points']*(0-1)
                waypoints.pop(waypoint)
            else:
                waypoints[waypoint] = {"stat": stat, "points": points, "char": self.ref}

            output = self.refname + " " + stat + " " + prefix + str(points)
            if stat == "lust":
                self.lust += points
            
            if stat == "love":
                self.love += points

            renpy.transition(dissolve)
            renpy.show_screen("PointsNotify", output)
            renpy.pause(3.0)
            renpy.transition(dissolve)
            renpy.hide_screen("PointsNotify")
I have defined the characters at first (maybe this wouldn't be totally neccessary, but did it anyway...):

Code: Select all

init -1:
    default cira = None
    default tapsin = None
    default vici = None
    default elyria = None
    default maeve = None
    default ida = None
    default miriam = None
And I initialized them in the start-label:

Code: Select all

    python:
        if cira is None:
            cira = Person("cira", "Kira", 26, 100, 100, 3)
        if tapsin is None:
            tapsin = Person("tapsin","Tapsin", 19, 100, 100, 2)
        if vici is None:
            vici = Person("vici","Vici", 28, 100, 100, 0) 
        if elyria is None:
            elyria = Person("elyria","Elyria", 280, 100, 100, 5)
        if miriam is None:
            miriam = Person("miriam","Miriam",22, 100, 100, 7)
        if maeve is None:
            maeve = Person("maeve","Maeve",25, 100, 100, 9)
        if ida is None:
            ida = Person("ida","Ida",25, 100, 100, 10)
So far so good. The gameplay goes on and girl after girl get activated like:

Code: Select all

$ cira.activate()
Later in the game the girls get points like:

Code: Select all

$ cira.pointsadd("love", 1,"16952d1e-9dd3-4d93-9413-8c65d823b60f")
Their stats are shown in a stats screen. Girl after girl are shown in the stats screen when the become active and I can see their love, lust, upkeep and location in the screen.

But what bothers me is: Each time I hit Shift+R all girls loose their stats and go back to .active = False.

I know, that is because of renpy loading everything until it hits the end of the start-label and first it sets the girls to None (because of the default) and then it sets them to the init-values of the Person-class. I found out by testing if initializing them in the next label (right after the start-label) would lead me to a better solution and all the girls variables were set to None.

So my question is:
Has anyone an idea how to preserve the values inside the classes-variables while reloading?

Best regards

Lacha

User avatar
jeffster
Miko-Class Veteran
Posts: 888
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Reloading the game resets my python Classes

#2 Post by jeffster »

Ren'Py is an engine built on top of Python.
It's not Python. Ren'Py has its own workflow, with specific processes of initialization and rollback.
Therefore we use "define" and "default" statements, rather than initializing variables in "init python" blocks.
More explanation:
https://renpy.org/doc/html/python.html

In particular, this is incorrect:

Code: Select all

init -1:
    default cira = None
init phase is for preparation of constant data, structures, class definitions etc.

Then "default" is a statement for the game start.

In other words, "default" data variables are set at the start, and then they can change, and Ren'Py tracks those changes and saves and loads them, keeping the values properly updated for rollbacks etc.

"init" data variables are either constant (in particular define's), or they don't need to be saved (being local or transitional variables or something like that).

Therefore, for objects that change and should be saved, we normally use "default".

(Sometimes it can be a bit confusing, if you need to use "default" data in functions defined at "init" stage, when "default" variables aren't yet defined. But read on...)

In general, we do like this:

Code: Select all

init python:
    # "Constant": class definition
    class Abc():
        def __init___(self, def):
            self.def = def

# Note that "default" is outside "init":
default something = this + that
default abc = Abc(something)
default my_list = []

label start:
    # Normally we use "default" for data we need at the start; but we can do this too:
    if abc not in my_list:
        $ my_list.append(abc)
And if at the "init" stage you need to refer to a variable that isn't defined at "init", you can use the fact that Ren'Py variables user data usually belong to "store" namespace:

Code: Select all

init python:
    do_something_with(store.that_variable)

    # "that_variable" will be defined (or let's say "defaulted") later.
    # But we can already refer to it as a field of "store" object.
If the problem is solved, please edit the original post and add [SOLVED] to the title. 8)

Lacha
Newbie
Posts: 12
Joined: Wed Mar 06, 2024 12:23 pm
Contact:

Re: Reloading the game resets my python Classes

#3 Post by Lacha »

Well... It kept me busy and I found out what was the problem.
It wasn't that renpy undid the girls variables... it was in the way I tried to access them...
When defined, I saved all of them in a list. And I was going to access them while iterating through that list.
It was the list that was reset to default and it was the list, that contained the classes as well...
I have changed the class code a bit:

Code: Select all

    class Person:
        def __init__(self, varname, name, age, maxlove, maxlust, upkeep):
            global characters
            self.varname = varname
            self.name = name
            self.age = age
            self.upkeep = upkeep
            self.love = 0
            self.lust = 0
            self.maxlove = maxlove
            self.maxlust = maxlust
            self.active = False
            self.tapsinname = name.replace("th", "t").replace("Th", "T").replace("TH","T")
            self.location = "nowhere"
            self.state = 1 
            if not self in characters:
                #characters.append(self)
                characters.append(self.varname)
            if self.name[-1] == "s":
                self.refname = self.name
            else:
                self.refname = name+"s"
            self.ref = self
and while iterating through the list i did this:

Code: Select all

for xchar in characters:
	char = globals()[str(xchar)]
	....
instead of

Code: Select all

for char in characters:
	....
Seemed to solve the whole thing...

Lesson learned: Don't save classes in lists.

Sometimes it helps to have to type it down or tell someone.

Thanks for being here.

Kind regards
Lacha

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1198
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Reloading the game resets my python Classes

#4 Post by m_from_space »

Lacha wrote: Wed Mar 06, 2024 1:49 pm Lesson learned: Don't save classes in lists.
Sorry to say it like this, but that's complete nonsense. @jeffster already explained a lot what is going wrong with your code, so I hope you read about it.

Code: Select all

init -1 python:
    characters = []
Here you're creating a variable, that Renpy will treat as a constant. So everything you change inside this list, Renpy will not save. Only use "default" and "define" for creating variables and constants respectively!

Correct:

Code: Select all

default characters = []

Code: Select all

init -1:
    default cira = None
    default tapsin = None
    default vici = None
    default elyria = None
    default maeve = None
    default ida = None
    default miriam = None
That's very unnecessary, the correct way would be:

Code: Select all

default cira = Person("cira", "Kira", 26, 100, 100, 3)
# ...
But what bothers me is: Each time I hit Shift+R all girls loose their stats and go back to .active = False.
That's because you created your variables in the start label, instead of using "default". Don't do that!

---
jeffster wrote: Wed Mar 06, 2024 1:22 pm In particular, this is incorrect:

Code: Select all

init -1:
    default cira = None
That is actually not wrong, but you don't have to put it inside an init block.

User avatar
jeffster
Miko-Class Veteran
Posts: 888
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Reloading the game resets my python Classes

#5 Post by jeffster »

Lacha wrote: Wed Mar 06, 2024 1:49 pm Thanks for being here.

Kind regards
Lacha
You are welcome!

Actually you can refer to Person's by individual (variable) names and also keep them in a list, see the code below. It works with no problem, including rollback, reload etc.

(It's more Pythonic (Renpyic) to have "characters" in "store" namespace, rather than global. And set them with "default" statement).

Code: Select all

init python:
    class Person:
        def __init__(self, varname, name, age, maxlove, maxlust, upkeep):
            self.varname = varname
            self.name = name
            self.stats = {"age": age, "upkeep": upkeep}
            self.max = {"love": maxlove, "lust": maxlust}
            setattr(store, varname, self)


        def add(self, pts, stat="love"):
            self.stats[stat] = min(
                                self.stats.get(stat, 0) + pts,
                                self.max[stat]
                                )


        def get(self, stat="love"):
            return self.stats.get(stat, 0)


        def __repr__(self):
            return '\n'.join((f'{self.name}: {self.get("age")} y.o.',
                    f'love: {self.get("love")}/{self.max["love"]}',
                    f'lust: {self.get("lust")}/{self.max["lust"]}',
                    f'upkeep: {self.get("upkeep")}'
                    ))


default characters = [
    Person("cira", "Kira", 26, 100, 100, 3),
    Person("tapsin","Tapsin", 19, 100, 100, 2),
    ]

label start:
    "Print characters' stats from Python loop:"
    python:
        for ch in characters:
            renpy.say("", ch.__repr__())

    "Print characters' stats with Ren'Py text substitution:"
    "[cira]"
    "[tapsin]"
    "OK!"

    "Now let's add in a loop: love cira -22, tapsin +22; lust +3 for both:"
label loop:
    $ cira.add(-22)
    $ tapsin.add(22)
    python:
        for ch in characters:
            ch.add(3, "lust")
    "[cira]"
    "[tapsin]"
    jump loop
Last edited by jeffster on Wed Mar 06, 2024 4:37 pm, edited 1 time in total.
If the problem is solved, please edit the original post and add [SOLVED] to the title. 8)

User avatar
jeffster
Miko-Class Veteran
Posts: 888
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Reloading the game resets my python Classes

#6 Post by jeffster »

m_from_space wrote: Wed Mar 06, 2024 3:34 pm

Code: Select all

init -1:
    default cira = None
That is actually not wrong, but you don't have to put it inside an init block.
Well, it is wrong, because "init -1" is executed before "init 0", and "default" there is executed after "init 0".
This construction tells us that it would be executed at "init -1", which is wrong.
It's a syntax that's trying to mislead those who read the code, and might provoke bugs.
If the problem is solved, please edit the original post and add [SOLVED] to the title. 8)

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1198
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Reloading the game resets my python Classes

#7 Post by m_from_space »

jeffster wrote: Wed Mar 06, 2024 4:34 pm Well, it is wrong, because "init -1" is executed before "init 0", and "default" there is executed after "init 0".
This construction tells us that it would be executed at "init -1", which is wrong.
It's a syntax that's trying to mislead those who read the code, and might provoke bugs.
No, you don't quite understand. By default it's executed at init time 0. But init blocks can be used to determine when something is executed.

So for example this wouldn't work:

Code: Select all

default mylist = [mystring]
default mystring = "hello"
But this works (for example if variables are inside different script files):

Code: Select all

default mylist = [mystring]

init -1:
    default mystring = "hello"

User avatar
jeffster
Miko-Class Veteran
Posts: 888
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Reloading the game resets my python Classes

#8 Post by jeffster »

m_from_space wrote: Thu Mar 07, 2024 6:18 am
jeffster wrote: Wed Mar 06, 2024 4:34 pm Well, it is wrong, because "init -1" is executed before "init 0", and "default" there is executed after "init 0".
This construction tells us that it would be executed at "init -1", which is wrong.
It's a syntax that's trying to mislead those who read the code, and might provoke bugs.
No, you don't quite understand. By default it's executed at init time 0. But init blocks can be used to determine when something is executed.

So for example this wouldn't work:

Code: Select all

default mylist = [mystring]
default mystring = "hello"
But this works (for example if variables are inside different script files):

Code: Select all

default mylist = [mystring]

init -1:
    default mystring = "hello"
Wow, that's unexpected, because ( https://renpy.org/doc/html/python.html# ... -statement )

> The default statement sets a single variable to a value if that variable is not defined when the game starts, or after a new game is loaded.

It would be intuitive to understand that "default" is related to some "start phase", after structures and constants were defined in "init" or "define"...
If the problem is solved, please edit the original post and add [SOLVED] to the title. 8)

Lacha
Newbie
Posts: 12
Joined: Wed Mar 06, 2024 12:23 pm
Contact:

Re: Reloading the game resets my python Classes

#9 Post by Lacha »

Hello and first of all:
Thank you for your replies.
I have not seen @jeffster s reply before I wrote my first reply. I am sorry, I didn't mean to be rude. I just didn't notice.
Both of you helped me really good to understand how renpy works.
I must say: I have read the renpy documentation but didn't understand it to the fullest.

I didn't expect that things like these work:

Code: Select all

default cira = Person(.....)
I also wasn't aware of this creating a constant:

Code: Select all

init -1 python:
    characters = []
Now things are clearer and I will rewrite my code and after I got it to work properly I will post an example of the working code as a solution or for your review if you'd be so kind.

Thanks to @jeffster and @m_from_space !!! You have enlightened me!

Best regards
Lacha

Lacha
Newbie
Posts: 12
Joined: Wed Mar 06, 2024 12:23 pm
Contact:

Re: Reloading the game resets my python Classes

#10 Post by Lacha »

Hello everyone,
I finally found some time to work over the class and put in some of your helpful hints.

Code: Select all

default characters = [
    Person("tapsin",        "Tapsin",       19,  100, 100, 1),
    Person("cira",          "Kira",         26,  100, 100, 5)
]
init python:
    def notify(output):
        renpy.transition(dissolve)
        renpy.show_screen("PointsNotify", output)
        renpy.pause(3.0)
        renpy.transition(dissolve)
        renpy.hide_screen("PointsNotify")
    class Person:
        def __init__(self, varname, name, age, maxlove, maxlust, upkeep, birthday=0):
            self.varname = varname
            self.name = name
            self.tapsinname = name.replace("th", "t").replace("Th", "T").replace("TH","T")
            if self.name[-1] == "s":
                self.refname = self.name
                self.tapsinrefname = self.tapsinname
            else:
                self.refname = self.name+"s"
                self.tapsinrefname = self.tapsinname+"s"
            self.stats = {
                "age":age,
                "upkeep":upkeep,
                "location":"nowhere",
                "lust":0,
                "love":0,
                "birthday": birthday,
                "state": 1, # example: kira.state = 1 : normal human | kira.state = 2 : succubus
                "mood": "neutral",
                "active": False
            }
            self.max = {
                "love": maxlove,
                "lust": maxlust
            }
            setattr(store,varname,self)
            self.ref = self

        def activate(self):
            self.set("active", True)
            shuffleOccupancy
            
        def pointsadd(self, stat, points, waypoint="generatedWaypoint"): #waypoint only for backwards compatibility
            if points >= 0:
                prefix = "+"
            else:
                prefix = ""
            output = self.refname + " " + stat + " " + prefix + str(points)
            self.set(stat, min(
                                (self.stats[stat] + points),
                                self.max[stat]
                                ))
            renpy.invoke_in_new_context(notify,output=output) #maybe not best practice, but it fixed errors when another screen is loaded

        def get(self, stat="love"):
            global rooms
            global dusts
            #the most usual thing: Just return the stats
            if stat in self.stats:
                retval = self.stats[stat]
            #some fancy variables that need handling before returning values
            if stat == "locationkey":
                retval = self.stats['location'].replace(" ","_")
            if stat == "location":
                if retval != "nowhere":
                    retval = rooms[retval.replace(" ","_")].fullname
            if stat == "upkeepDusts":
                retval = self.stats['upkeep']
                retval = str(f"{retval:0.2f}")+dusts
            return retval
	#yes I could just set it in the code, but wanted a function for it in case I would want something else to happen
        def set(self, stat="love", val=0):
            self.stats[stat] = val
	#nice sh*t, thanks for that!!!
        def __repr__(self):
            return '\n'.join((f'{self.name}: {self.get("age")} yo',
                    f'love: {self.get("love")}/{self.max["love"]}',
                    f'lust: {self.get("lust")}/{self.max["lust"]}',
                    f'upkeep: {self.get("upkeepDusts")}',
                    f'room: {self.get("location")}'
                    ))

in the stats window I can now get the stats easily:

Code: Select all

            python: 
                char = tapsin #for example
                pic = "sprites/squaresprites/sq_"+char.name.lower()+" "+str(char.get("state"))+".png"
                img_x = 75
                img_y = 180
                char_pic_width = 210
                char_pic_height = 210
                text_x = img_x+char_pic_width
                text_y = img_y           
                char_desc_width = 210
                char_desc_height = 210     
            frame:
                pos(img_x, img_y)
                xysize(char_pic_width, char_pic_height)
                background Solid("#1E6639")
                imagebutton:
                    pos(15, 24)
                    background Solid("#1E6639")
                    idle im.Scale(pic, 180,180)

            frame:
                pos(text_x,text_y)
                xysize(char_desc_width, char_desc_height)
                background Solid("#1E6639")
                vbox:
                    xalign 0
                    text "{size=18}[char]{/size}"
                    if persistent.cheat == True:
                        if char.get("active") == False:
                            textbutton "{size=18}Active: no{/size}" action Function(char.set,"active",True)
                        else:
                            textbutton "{size=18}Active: yes{/size}" action Function(char.set,"active", False)
And midgame I can now do some of the following:

Code: Select all

$ tapsin.set("active",True) #activates the girl for showing up in stats and being shuffled into a room or so
$ tapsin.pointsadd("love",1) #adds one lovepoint
$ room = tapsin.get("location") #returns the room she is shuffled in.
And so on...

Now also the Reload issue is fixed and my girls work like a charm!

Thank you so much for your help!!

Best regards

Lacha

Post Reply

Who is online

Users browsing this forum: Google [Bot], Semrush [Bot]