Problem with rollback and inventory.

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
Hello guys, i have a little problem with my inventory system.

I recently started using classes rather than a dictionary so it's easier for updating the game. Everything works great, saving works, except for one thing: rollback.

When i remove an item from the inventory, then rollback to before the item was removed, the item is still missing.

Example: The mc needs flowers for his first date with his new GF. The mc has one flower bouquet in his inventory. After the choice to go on the date is selected, the flowers are correctly removed from the inventory. But when i rollback before the choice is made, the number of flowers is still at 0, they are not "put back" in the inventory. Note that the money, wich is a simple variable is working and the amount used on the date is "refunded".

This is my inventory system (lifted from the cookbook and slightly modified.):

Code:
init python:
    class Item(object):
        def __init__(self, name):
            self.name = name

    class InvItem(object):
        def __init__(self, item, amount):
            self.item = item
            self.amount = amount
    class Container(object):
        def __init__(self):
            self.inventory = []

        def add_item(self, item, amount=1):
            if item in [i.item for i in self.inventory]:  # I can't believe I got this line to work with my first try
                    self.finditem(item).amount += amount   # oh god why
            else:
                self.inventory.append(InvItem(item, amount))
            return('success')

        def has_item(self, item, amount=1):
            if item in [i.item for i in self.inventory]:
                if self.finditem(item).amount >= amount:
                    return(self.finditem(item).amount)
                else:
                    return(False)
            else:
                return(False)

        def finditem(self, item):
            return(self.inventory[[i.item for i in self.inventory].index(item)])

        def remove_item(self, item, amount=1):
            if self.has_item(item):
                self.finditem(item).amount -= amount
                if self.finditem(item).amount <= 0:
                    self.inventory.pop(self.inventory.index(self.finditem(item)))
                    return('gone')
                else:
                    return('more left')
            else:
                return('not found')


$ backpack = Container()

And this is the menu choice removing the item from the class

Code:
menu:
    "{color=FFFF00}Ask her out on a date (-100$){/color}" if backpack.has_item (flowers) >= 1 and elisa_var == 1:
                    if money_var <= 99:
                        "I don't have enough money to take her out."
                        jump elisaroom_night_loop
                    else:
                        $backpack.remove_item (flowers)
                        $money_var -= 100
                        jump elisalvl1_label

I tried to use to "default" the classes before the start label but it doesn't work. And my google-fu is not strong enough to find a solution. Would any of you ge**could help me? It would be much appreciated.
 

Epadder

Programmer
Game Developer
Oct 25, 2016
568
1,064
I didn't double check that this solves it, but it's probably the culprit. :whistle:

The standard 'object' you're using to inherit from doesn't support rollback, Ren'py has a different base object to use 'renpy.python.RevertableObject' for supporting rollback.

So change 'object' to 'renpy.python.RevertableObject' in your code.
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
Sadly it did not work Epadder. But i think you are onto something. Searching a little more it do seems like renpy rollback function has a problem when a method inherit from another class. (don't knopw if i use the proper terms here).

Gonna try to look more into it, thank you. I could just disable rollback for that portion but i would prefer it to work properly.
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
I am lost. I read on lemmasoft that i should use 'store.object' rather than 'object" and use 'import renpy.store as store' 'import renpy.exports as renpy' before, but it just doesn't work.

I tried also to put the code for the classes before and after the start label to no avail.

It seems that renpy rollback do not work properly with code inside an init block.

A way to make it work must exist but i am a beginner when it come to coding so...
 

drKlauz

Newbie
Jul 5, 2018
40
24
This code uses custom class and works well both with save/load and rollback.
Code:
init python:
  class Thing(object):
    def __init__(self,initial_x=0):
      super(Thing,self).__init__()
      self.x=initial_x

default thing=Thing(5) ## create class instance this way

label start:
#  $thing=Thing(5) ## or this way
  "test [thing.x]"
  $thing.x+=3
  "test2 [thing.x]"
  $thing.x=123
  "test3 [thing.x]"
  "done"
  return
 
  • Like
Reactions: Vanderer

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
Thanks DrKlauz.

I guess my error was not creating the class instance. What i don't get is what argument i should give to my classes instances when i "default" them.

In your example, what the "(5)" from "default thing=Thing(5)" come from?

Sorry for my noobness.
 

drKlauz

Newbie
Jul 5, 2018
40
24
Thing class take 1 optional argument, "initial_x". If you make object from class without argument default thing=Thing() then default value will be used - 0.

Code:
init python:
  class Thing(object):
    def __init__(self):
      super(Thing,self).__init__()
      self.x=0

default thing=Thing()
This is normal too, class can have multiple or none arguments in __init__ method. Also there are catch-all things, like *args and **kwargs, but for this i suggest reading python tutorials.

- renpy currently uses python 2.7 internally, but "soon(tm)" will migrate to 3.x
Be sure to inherit your classes from object or other classes inherited from object. Don't forget to call super() where it make sense.
 
  • Like
Reactions: Vanderer

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
I see, I am just to much of a beginner to use classes it seems.

I just don't undertand what argument i should put when i do that :

Code:
default item = Item()
default invitem = InvItem()
default container = Container()
Then i get that traceback:

Code:
While running game code:
  File "renpy/common/00start.rpy", line 189, in script
    python:
  File "renpy/common/00start.rpy", line 189, in script
    python:
  File "renpy/common/00start.rpy", line 190, in <module>
    renpy.execute_default_statement(True)
  File "game/script.rpy", line 148, in set_default
    default item = Item()
  File "game/script.rpy", line 148, in <module>
    default item = Item()
TypeError: __init__() takes exactly 2 arguments (1 given)



I think i will just disable rollback at the moment the menu choice is selected so the user cannot rollback passt that point.

And start reading more about classes. But it's not easy because most python classes avaiable online are not for renpy in particular and things like rollback compatibility are not taken into account.

But thanks for the help, i will read the documentation you gave me.
 

drKlauz

Newbie
Jul 5, 2018
40
24
Don't worry, everyone starts from start.

For Item it is
Code:
default apple=Item("Apple")
Because Item.__init__ method has 2 arguments:
- self, this is special argument pointing to item object, you do not normally provide this, as python do it automatically (read: magic happens)
- name, this argument have no default value, so you need to provide it

You don't instantiate InvItem objects, it is internally used by container i understand.
To create backpack you do
Code:
default backpack=Container()
Things under "init python" or after $ uses pretty much straightforward python 2.7, so any general python class/tutorial is good.
 
  • Like
Reactions: Vanderer

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
Haaa, ok, but i already did that!

Code:
default backpack = Container()   ###that one was in the code of the OP
default flowers = Item("Flowers")  ###that one was not in the op but was in my code
Except i did use "$" rather than "default". But trying with "default" don't change anything: rollback doesn't work. I tried to put it before and after the label start, but no change.

The number of flowers when i choose the menu "Ask her out on a date" is reduced by one (from 1 to 0 in testing) as it should be. But when i roll back the number of flowers in inventory stay at 0.
 

drKlauz

Newbie
Jul 5, 2018
40
24
Check this, if it works as expected then problem was due to missing super() calls.
Code:
init python:
    class Item(object):
        def __init__(self, name):
            super(Item,self).__init__()
            self.name = name

    class InvItem(object):
        def __init__(self, item, amount):
            super(InvItem,self).__init__()
            self.item = item
            self.amount = amount
    class Container(object):
        def __init__(self):
            super(Container,self).__init__()
            self.inventory = []

        def add_item(self, item, amount=1):
            if item in [i.item for i in self.inventory]:  # I can't believe I got this line to work with my first try
                    self.finditem(item).amount += amount   # oh god why
            else:
                self.inventory.append(InvItem(item, amount))
            return('success')

        def has_item(self, item, amount=1):
            if item in [i.item for i in self.inventory]:
                if self.finditem(item).amount >= amount:
                    return(self.finditem(item).amount)
                else:
                    return(False)
            else:
                return(False)

        def finditem(self, item):
            return(self.inventory[[i.item for i in self.inventory].index(item)])

        def remove_item(self, item, amount=1):
            if self.has_item(item):
                self.finditem(item).amount -= amount
                if self.finditem(item).amount <= 0:
                    self.inventory.pop(self.inventory.index(self.finditem(item)))
                    return('gone')
                else:
                    return('more left')
            else:
                return('not found')

default apple=Item("Apple")
default backpack = Container()

label start:
  $backpack.add_item(apple,5)
label eating_apples:
  if backpack.has_item(apple):
    $count=backpack.finditem(apple).amount
  else:
    $count=0
  menu:
    "You have [count] apples"
    "eat apple" if backpack.has_item(apple):
      $backpack.remove_item(apple)
      "yum-yum"
      jump eating_apples
    "done":
      pass
  "done"
  return
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
So, your code in a new project works, but not when i implement the first part ,before the start label, in my own code...

Could it be because how my menu choice is coded? Or perhaps because the menu is in a screen?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,964
16,207
The standard 'object' you're using to inherit from doesn't support rollback, Ren'py has a different base object to use 'renpy.python.RevertableObject' for supporting rollback.
I don't remember exactly in what version it happened, but now when you define a class in a rpy file, Ren'py will automatically use renpy.python.RevertableObject when you don't specify an ancestor, or specify object as ancestor.


Except i did use "$" rather than "default".
The $ backpack = Container() in your example here was outside a label, what is totally wrong.


But trying with "default" don't change anything: rollback doesn't work.
You tried by restarting a totally new game, right ? If the problem come from variable savability, changing to default will not change the behavior of the variable if you use a save file.


I tried to put it before and after the label start, but no change.
default statement are proceeded at init level whatever where they are placed.


Check this, if it works as expected then problem was due to missing super() calls.
The error can not be due to the missing super. It just call the __init__ method of the ancestor and, as its name say, this method just initialize the object. It don't create it, it don't change its internal behavior or anything, it just put a default value to the attributes. And the __init__ method of the object class just do nothing.


So, your code in a new project works, but not in my own code...

Could it be because how my menu choice is coded?
Try with your project, but by starting a new game instead of loading a save file... I'm 95% sure that (now that you use default to create "backpack" it will works fine.
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
I tried with a new game every single time anneo'nymous. But thanks for your help.
 

drKlauz

Newbie
Jul 5, 2018
40
24
The error can not be due to the missing super. It just call the __init__ method of the ancestor and, as its name say, this method just initialize the object. It don't create it, it don't change its internal behavior or anything, it just put a default value to the attributes. And the __init__ method of the object class just do nothing.
Comment super() calls and try it. I seen some rare, but reproducible behavior without calls.
Steps:
- select "eat apple" until you have 0 apples
- select "done"
- press page_up/rollback 3 times
If you can't reproduce it, then i really don't know :D
Not sure what is reason for this, but adding super solved it. In this particular case. Plus never seen anything like this in my own projects.

P.S.: Checked renpy object class, indeed nothing really should be different. My last guess is mouse wheel glitch or something what triggered dismiss/rollforward right after rollback. Weird.
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
You are right, with your poject it doesn't work when your roll back by one increment, but one the second increment you have two apple.

But using super in my own project soesnt work.
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
If someone want to look at my project, here a link of it (it's WIP of course, please don't judge).



script.rpy is were my inventory code is, elisaroom.rpy is where the choice that cause proble is located.


First save is just before the choice, you have one flowers in the inventory (character icon left of the map icon).
Talk to the girl, choose the yellow menu choice, then rollback. Iventory chow 0 flowers.



Complete steps to reproduce from new game:

-enter name
-pass intro until you arrive to a menu choose "i have no more questions"
-next menu choose anything you want
-you should be in a bedroom
-open map "upper right icon)
-go to shop (third icon)
-buy flower(inventory(icon of a man left of the map) show 1 flower)
-then bye
-go back home
-inside the house
-pass time until "late evening"
-go to the second story and take the door to the left (elisa's room)
-talk to her and select the yellow menu choice (it's the one causing problem)
-try to rollback and look at inventory





I would understand if you don't have time though and i am already very thankful for the help you guys provided.
 
Last edited:

Epadder

Programmer
Game Developer
Oct 25, 2016
568
1,064
Okay... I found where rollback is breaking... but not why. :whistle:

If you remove it from the inventory the amount will not rollback, but if you have more than 1... the amount does rollback.

Maybe anne O'nymous will have more insight as to why that is... I suspect it has something to do with all the nesting. :unsure:

===EDIT===

Here's the solution to fix the issue... at least.

Python:
def remove_item(self, item, amount=1):
            if self.has_item(item):
                if self.finditem(item).amount - amount <= 0:
                    self.inventory.pop(self.inventory.index(self.finditem(item)))
                    return('gone')
                else:
                    self.finditem(item).amount -= amount
                    return('more left')
            else:
                return('not found')
Only change the amount when there is more than 1 left, instead just remove the item if the amount would go to 0 or below.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,964
16,207
I suspect it has something to do with all the nesting. :unsure:
It's something more complex than that. If it was only due to the nesting, then the bug would happen even with other values, but you said that it happen only when it pass from 1 to 0 ; it's surely more from X to 0, whatever the value of X.

But honestly my brain isn't working enough, I totally miss what could be the cause :(
 

Vanderer

Active Member
Game Developer
Dec 8, 2017
739
2,469
Okay... I found where rollback is breaking... but not why. :whistle:

If you remove it from the inventory the amount will not rollback, but if you have more than 1... the amount does rollback.

Maybe anne O'nymous will have more insight as to why that is... I suspect it has something to do with all the nesting. :unsure:
THANK YOU VERY MUCH!!!

I did try with 2 items in the inventory and you are perfectly right.

And the code provided does work. And it has the advantage of clearing the inventory of the item when you remove the last one, wich is less clutter. So, killing two birds with one stone, very nice.

So thanks again Epadder, you helped me a lot, it would have bugged me to no end to have faulty code in my game, and to have to disable rollback.

Also thanks to drKlaus and anne O'nymous for their help too.
 
  • Like
Reactions: Epadder