Ren'Py Unexpected behavior of Ren'Py [Solved]

GNVE

Active Member
Jul 20, 2018
653
1,129
So I have a building function to build new stuff and a different function to have a timer run out before the space is available.
I made a little test screen to build stuff and notice that if I use the function several times in a row it will trigger correctly (as I see the space available decrease) but it is a guess if eventually all buildings actually show up. (e.g. if I build 10 cabinet offices 5-8 are actually build eventually). Now I don't know yet how often players will run into this issue but it seems wise to prevent the issue in the first place. This happens both if I use it 5 times in a row or wait for the maintenance function to trigger in between.

P.s. I know the code is not the most amazing as I should probably parent the commenhandlerclass but this way I understand what I'm doing... :)

All relevant code (I think):
Python:
init python:
    #the building function does the building the maintenance is triggered once per day.
    class commonhandlerclass:
        def building(self, OID, amount, what, remove=False): #building new buildings/offices/classrooms etc
            if what == None:
                if remove == False:
                    globals()[OID].money -= globals()[OID].space['unallocated']['icps'] * amount * globals()[OID].mod['constructioncost']
                    globals()[OID].store['timers']['building'].append(('unallocated',globals()[OID].space['unallocated']['T'],amount))
                else:
                    globals()[OID].money = globals()[OID].base['removalcost'] * amount * globals()[OID].mod['constructioncost'] * 10
                    globals()[OID].space['unallocated']['total'] -= amount

            else:
                if remove == False:
                    size = globals()[OID].space[what]['size'] * amount
                    cost = globals()[OID].space[what]['icps'] * size * globals()[OID].mod['constructioncost']
                   
                    if globals()[OID].space['unallocated']['total'] >= size:
                        globals()[OID].space['unallocated']['total'] -= size
                        globals()[OID].money -= cost
                        globals()[OID].store['timers']['building'].append((what, globals()[OID].space[what]['T'],amount))
                else:
                    size = globals()[OID].space[what]['size'] * amount
                    cost = globals()[OID].base['removalcost'] * size * globals()[OID].mod['constructioncost']
                   
                    globals()[OID].space['unallocated']['total'] += size
                    globals()[OID].space[what]['total'] -= amount
                    globals()[OID].money -= cost


        def transfer(self, OID, tuple): #transfering money/buildingspace/ etc
            pass


        def maintenance(self, OID):
            temp = []
            for i in globals()[OID].store['timers']['building']:
                if i[1] <= 0:
                    globals()[OID].space[i[0]]['total'] += i[2]
                    globals()[OID].store['timers']['building'].remove(i)
                else:
                    temp.append((i[0],i[1]-1,i[2]))
           
            globals()[OID].store['timers']['building'] = temp
           
    class campusclass: #population = students + workers
        def __init__(self, OID='campus', name='Turner University', money=250, corruption=0, prestige=-1000, space={}, base={}, mod={}, store={}, stats={}):
            self.OID = OID
            self.name = name
            self.money = money
            self.corruption = corruption
            self.prestige = prestige
            self.space = space
            self.base = base
            self.mod = mod
            self.store = store
            self.stats = stats

            if not 'timers' in store: store['timers'] = {'building':[]}
            if not 'constructioncost' in mod: mod['constructioncost']= 1
            if not 'total' in space: space['total'] = 0
            if not 'unallocated' in space: space['unallocated'] = {'total':0, 'T':15,'icps':1500, 'ccps':1.0}
            if not 'cabinetoffice' in space: space['cabinetoffice'] = {'total':0, 'seats':1, 'size':10, 'T':2, 'icps':1000.0, 'ccps':10.0, 'ccpo':10.0, 'pop':'cabinet', 'demand':1.0} #cost per size vs cost per occupant negative numbers give income.
            if not 'supportoffice' in space: space['supportoffice'] = {'total':0, 'seats':5, 'size':25, 'T':3, 'icps':100.0, 'ccps':1.5, 'ccpo':1.0, 'pop':'admin', 'demand':1.0} #pop means who uses this, demand is how likely to use/want this. If demand is not met happiness will drop
            if not 'toilet' in space: space['toilet'] = {'total':0, 'seats':9, 'size':10, 'T':5, 'icps':200.0, 'ccps':2.5, 'ccpo':5.0, 'pop':'population', 'demand':0.063}
            if not 'storage' in space: space['storage'] = {'total':0, 'seats':2, 'size':10, 'T':1, 'icps':20.0, 'ccps':1.1, 'ccpo':1.0, 'pop':'janitor', 'demand':1.0} #150m2 per hour 1200m2 per shift


        def EOD(self):
            commonhandler.maintenance(self.OID)
           
screen TSTbuildSCR():
    $renpy.retain_after_load()
    $unallocated = globals()['campus'].space['unallocated']['total']
    $CO = globals()['campus'].space['cabinetoffice']['total']
    $SO = globals()['campus'].space['supportoffice']['total']

    vbox:
        hbox:
            text "you currently have [CO] cabinetoffices and [SO] supportoffices. there is [unallocated] space left"
        hbox:
            button:
                action Function(globals()['commonhandler'].building, 'campus', 200, None)
                text 'build 200m2 of building space.'
        hbox:
            button:
                action Function(globals()['commonhandler'].building, 'campus', 1, 'cabinetoffice')
                text 'build a cabinetoffice'
        hbox:
            button:
                action Function(globals()['commonhandler'].building, 'campus', 5, 'supportoffice')
                text 'build 5 supportoffices'
        hbox:
            button:
                action ToggleScreen("TSTbuildSCR"), ToggleScreen('TSToverlaySCR')
                text 'back'
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
P.s. I know the code is not the most amazing as I should probably parent the commenhandlerclass but this way I understand what I'm doing... :)
Before parenting this or that, it should first be more explicit.
Nowhere in the code do you need to rely on globals()[variableName]. At anytime in a RPY file, variables are available in the local store through the "store" object. Therefore, OID = getattr( store, OID ), and store.campus would make your code more explicit.

It's also possible that it fix few issues. I'm not sure to what extend globals share the same content in a screen that in functions. Therefore, addressing the variables directly where they are effectively located would ensure that each part of the code actually use the same values.

As for the issue itself, I don't really see from where it can come.
Is it possible that it don't come from that part of the code, but from the loop calling commonhandlerclass.building that would punctually miss a step ?
 

GNVE

Active Member
Jul 20, 2018
653
1,129
Before parenting this or that, it should first be more explicit.
Nowhere in the code do you need to rely on globals()[variableName]. At anytime in a RPY file, variables are available in the local store through the "store" object. Therefore, OID = getattr( store, OID ), and store.campus would make your code more explicit.
That came about because of good old googling for issues I had at some point with the screen language. and then it's the principle of when you have a hammer everything is a nail.
but if I understand you correctly: if I use OID = getattr( store, OID ) then I could follow that with OID.func(a,b)?
It's also possible that it fix few issues. I'm not sure to what extend globals share the same content in a screen that in functions. Therefore, addressing the variables directly where they are effectively located would ensure that each part of the code actually use the same values.
So far I haven't seen (m)any issues but I'll rewrite the code to see if it fixes anything.
As for the issue itself, I don't really see from where it can come.
Is it possible that it don't come from that part of the code, but from the loop calling commonhandlerclass.building that would punctually miss a step ?
Wouldn't that make the issue appear at times when I press the button once? So far it only happens if I press the button multiple times. pressing it once, advancing time enough for the maintenance function to run through the timer, then pressing it again etc. seems to work fine (did enough tests).
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
but if I understand you correctly: if I use OID = getattr( store, OID ) then I could follow that with OID.func(a,b)?
Well, if OID have a "func" method, yes.

Explanation:

Through globals()[OID] you access the object stored in a variable whose name is stored in "OID".

But in Python functions and classes defined in a RPY file, "global" point to a copy of the "store" object where Ren'Py store all the variables used by the game.

Therefore, you can access the same variable by getting it directly from "store", with getattr( store, OID ).

Then I simply simplify the code by doing a translation. You pass the name of the variable to your code, and inside it transform the name into the object itself ; OID = getattr( store, OID ).



So far I haven't seen (m)any issues but I'll rewrite the code to see if it fixes anything.
As I said, I'm not sure if there's issue or not. All depend if Ren'Py redefine the global scope for screens or not.

By default, globals() contain the variables defined at main scope (the lowest possible scope, where every functions and classes is created). BUT the game works at an higher level scope.
Globally speaking:
  1. Python main scope
  2. Ren'Py core scope
  3. Game scope
You are at level 3, while globals() is at level 1.

For Python code embedded in RPY files, Ren'Py do a bit of magic, updating globals() to point to the variables at level 3. What make the code works in your classes and functions.
But screens aren't Python code. So, if Ren'Py do not update globals(), it will point to the variables at level 1, that are different. The code will not break, because you create the variables when needed, but like they are at a different scope level, they also are different variables.

Now, as I said, all this depend on the way Ren'Py treat globals() for screens. But it can't harm to directly address them through "store", and can solve some invisible subtle issues.


Wouldn't that make the issue appear at times when I press the button once?
It depend on the cause.

Let's say that the loop looks like this:
Python:
for atom in listOfCurrentBuild:
   atom.building( [...] )
And in between, you've code like that:
Python:
listOfCurrentBuild = refreshBuildInProgress()
if newBuildAsked:
    addABuild()
With "addABuild" filtering "listOfCurrentBuild", by example to avoid duplicated entries.

Every time the player the player will starts one, "listOfCurrentBuild" will be truncated, and the loop will miss some entries.
Like "listOfCurrentBuild" is refreshed at the starts of every cycle, every time the player do NOT starts a new build, the loop will proceed everything.


So far it only happens if I press the button multiple times. pressing it once, advancing time enough for the maintenance function to run through the timer, then pressing it again etc. seems to work fine (did enough tests).
What can be explained by the globals() Vs store part. If what exist in one do not exist in the other, a single entity would works. But starting to add entities would overwrite the previously defined ones.
It can also be explained by the kind of issue I talked about above, with the list being altered when you add an entity.


Side note:
Like all this massively rely on the notion of scope, don't hesitate to said it if there's part(s) of my explanation that you don't understand. The notion of scope isn't necessarily easy to understand at first, and even less to explain once it become something natural for you.
 

GNVE

Active Member
Jul 20, 2018
653
1,129
Yeah, I think I understand your explanation and can see how it can lead to issues. It also explains some other weirdness I found when using the console. As I have stated before (and probably will again) I do not have any business making a game like this (management game) but so far I'm having fun. I'm trying to do coding way above my skill level. With some help and a lot of googling I also seem to be getting somewhere. Learning a lot in the process as well. :)
 

GNVE

Active Member
Jul 20, 2018
653
1,129
So I changed out all the code to use the store. It seems to consistently build half the number I ask for (so clicking the button once I add 1N clicking it twice 1N clicking it 3 times 2N 4 2N etc. Even weirder is that if I build 5 and the next T interval another 5 it will build 3N and then 2N. (This is repeatable)

Also weird if you go to the main menu and start a new game it doesn't start at the default values for the building space but the value of the last game state. while the time function does return to the default.
 

peterppp

Member
Mar 5, 2020
480
884
So I changed out all the code to use the store. It seems to consistently build half the number I ask for (so clicking the button once I add 1N clicking it twice 1N clicking it 3 times 2N 4 2N etc. Even weirder is that if I build 5 and the next T interval another 5 it will build 3N and then 2N. (This is repeatable)
removing the element during iteration like you do looks dangerous. not sure how that works in python but it could potentially cause the type of problem you're describing
Code:
for i in globals()[OID].store['timers']['building']:
    if i[1] <= 0:
        globals()[OID].space[i[0]]['total'] += i[2]
        globals()[OID].store['timers']['building'].remove(i) # <--possible cause
    else:
        temp.append((i[0],i[1]-1,i[2]))
 
Last edited:
  • Like
Reactions: gojira667

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
Also weird if you go to the main menu and start a new game it doesn't start at the default values for the building space but the value of the last game state. while the time function does return to the default.
It's why all variables need to be declared with default.

If they are declared in an init block, or through define, that declaration will only happen once, right before Ren'Py display the start menu for the very first time. And this lead to the behavior you describe, the variables are not reset when you starts a new game.
 

GNVE

Active Member
Jul 20, 2018
653
1,129
removing the element during iteration like you do looks dangerous. not sure how that works in python but it could potentially cause the type of problem you're describing
Code:
for i in globals()[OID].store['timers']['building']:
    if i[1] <= 0:
        globals()[OID].space[i[0]]['total'] += i[2]
        globals()[OID].store['timers']['building'].remove(i) # <--possible cause
    else:
        temp.append((i[0],i[1]-1,i[2]))
Maybe, but tuples cannot be changed. That was the reason I did it like that. but I'll see if I can think of another way of doing it.
It's why all variables need to be declared with default.

If they are declared in an init block, or through define, that declaration will only happen once, right before Ren'Py display the start menu for the very first time. And this lead to the behavior you describe, the variables are not reset when you starts a new game.
unfortunately everything is defaulted already. (just checked to make sure).
 

peterppp

Member
Mar 5, 2020
480
884
Maybe, but tuples cannot be changed. That was the reason I did it like that. but I'll see if I can think of another way of doing it.
can't you just not remove the element? what's the point? you're using the temp later
 
  • Like
Reactions: GNVE

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
unfortunately everything is defaulted already. (just checked to make sure).
Hmm... Therefore everything is reset when you hit the start button, what mean that you shouldn't have that kind of behavior :/

I'm stuck, I don't see what can cause this, outside of what I said.
 

GNVE

Active Member
Jul 20, 2018
653
1,129
can't you just not remove the element? what's the point? you're using the temp later
Right sometimes it is so easy to complicate things. I'll try to see if it works.
Hmm... Therefore everything is reset when you hit the start button, what mean that you shouldn't have that kind of behavior :/

I'm stuck, I don't see what can cause this, outside of what I said.
no problem. I'll ask on the lemmasoft forums. Maybe I'm lucky and PyTom will bite :)
 

GNVE

Active Member
Jul 20, 2018
653
1,129
Maybe a force restart of Ren'py when returning to the main menu could be an option if all else fails.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
Maybe a force restart of Ren'py when returning to the main menu could be an option if all else fails.
No.

You would get rid of the issue, when you restart the game, but the bug wouldn't be solved. And the thing is that, if some variables aren't reset when you restart the game, then some can also possibly not be reset when you load a save file.
 
  • Like
Reactions: GNVE

GNVE

Active Member
Jul 20, 2018
653
1,129
No.

You would get rid of the issue, when you restart the game, but the bug wouldn't be solved. And the thing is that, if some variables aren't reset when you restart the game, then some can also possibly not be reset when you load a save file.
Yeah, you definitely have a point there. Hadn't thought about that. Thanks!
 

Turning Tricks

Rendering Fantasies
Game Developer
Apr 9, 2022
863
1,857
After a little back and forth on the LemmaSoft forums it seems that the store={}, in my classes was the problem. when I replaced store with storage the issue was solved.
Is it because store is one of these?

 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,244
15,016
Yeah probably. Didn't think it would be a problem because it isn't a variable name but apparently I was wrong. Ah well how else was I going to learn... :ROFLMAO:
Open the console, then type len( store.__dict__.keys() ).

You just replaced the dict-like object where Ren'Py store everything directly regarding the game :ROFLMAO:
 
  • Haha
Reactions: GNVE