I have been struggling for the past few days with a very strange issue with Ren'py not properly saving/restoring a persistent dictionary.
I was trying to implement a dynamic CG gallery to display images that are not declared as ren'py images (I won't get into the various reasons for this here, but the idea is that players can use their own image packs).
Those images are shown within specific screens, and in turn those screens track the file path for the images that have been seen as 'True' in a persistent dictionary. This more or less emulates the way renpy.seen_image() works for regular renpy images.
Everything works fine and dandy as long as Ren'py is running. Images unlock and display all right in the CG Gallery, and the dictionary works fine. A 'renpy.full_restart()' works just fine too.
However, when closing and restarting the game or doing a 'renpy.utter_restart()', all hell breaks loose.
The persistent dictionary disappears, all stored values are removed and the CG gallery is empty. I narrowed the problem down to the persistent dictionary itself disappearing: the code for the gallery seems not to matter. It's not just the individual values that are reset, the dictionary itself is gone.
On a few occasions, when only a handful of pictures were unlocked, the dictionary saved properly. But that was a very rare case. In most cases, the dict simply resets. I strongly suspect that something is messing with Ren'py's system for saving and/or restoring persistent values, because on occasion other persistent values from the game (such as the 'persistent.seen_intro' value I use to track a new game) have gone missing as well, even though they share zero code with the offending script. Those errors, if they happen, go completely silent.
It does not seem to be related to the method I use to declare the dict. I have tried using :
Code: Select all
if persistent.seen_dict is None:
persistent.seen_dict = defaultdict(bool)
I also don't think it's because I am using a DefaultDict: I am using other DefaultDicts as persistent variables in the game (for some menu options) without any trouble. I have also tried variants with a regular dictionary, or even just a plain list: the same bug happens either way.
I should also mention that I have used the 'Delete persistent' option in the Ren'py launcher to make sure previous attempts didn't mess with the persistent data. I have also played around with the init order, but didn't notice any difference.
So, I'm at a complete loss to explain what is going on here. Things that could maybe play a role (but I am not sure how to work around them right now):
- The image paths are long: The image file paths used as keys for the dictionary are strings that can be quite long and run over, say, 60 or 70 characters. I'm not sure why that would be a problem for a modern system, but, there.
- The number of images can be large: several hundreds. Does that somehow 'overload' the dictionary with too many keys? I'm not sure, but the fact that it doesn't reset when only a few pictures are unlocked could suggest that.
- The image paths contain '/' characters: Again, I'm not sure why it would mess with Ren'py saving persistent variables, but that's the only specificity I could think of compared to the other dictionaries I use.
The code for the game itself is very complex and largely unrelated, but I have included the relevant bits below. I have double-checked the whole code, and nothing else messes up with or even accesses the dictionary in question.
If anyone can help, you are my only Hope.
Code: Select all
init -999 python:
from collections import defaultdict
init -3:
define persistent.seen_dict = defaultdict(bool)
init -2 python:
IMGFORMATS = (".jpg", ".jpeg", ".png",".bmp", ".gif")
def is_imgfile(file):
if (file[-4:] in IMGFORMATS or file[-5:] in IMGFORMATS):
return True
return False
## This method checks if an image has been seen using the persistent dictionary or the regular renpy.seen_image.
## It works fine as long as the game is running.
def was_seen(pic): # Where pic is a String, either a file path or a renpy image name
if is_imgfile(pic): #
if persistent.seen_dict[pic]:
return True
elif renpy.seen_image(pic) or persistent.seen_dict[pic]:
return True
return False
def list_girl_packs(): # Returns a list of girl pack folders. Used for CG gallery
pack_list = []
for file in [f for f in renpy.list_files() if f.startswith("girls/")]:
file_parts = file.split("/")
if file_parts[1] not in pack_list:
pack_list.append(file_parts[1])
return pack_list
def init_galleries(): # This is run on start-up in an init block. The bug doesn't seem to originate from the gallery code itself, even though it's unwieldy.
global gp_gallery
gp_gallery = defaultdict(bool)
for pack in list_girl_packs():
gp_gallery[pack] = Gallery()
gp_gallery[pack].pics = [] # Not a native parameter for the Gallery object, used here for convenience
gp_gallery[pack].slideshow_delay = 1.0
gp_gallery[pack].navigation=True
gp_gallery[pack].span_buttons=True
gp_gallery[pack].locked_button=lock
for file in [f for f in renpy.list_files() if f.startswith("girls/" + pack) and is_imgfile(f)]:
gp_gallery[pack].button(file)
gp_gallery[pack].condition("was_seen('" + file + "')")
gp_gallery[pack].image(file)
gp_gallery[pack].pics.append(file)
gp_gallery[pack].renpy_images = False
#### SCREENS ####
## This screen is used to display non-renpy pictures
screen show_event(event_pic, x = None, y = None, proportional = True, bg = "#000"):
tag show_event
zorder 0
frame:
background bg
if event_pic:
$ persistent.seen_dict[event_pic.path] = True # This is how the game tracks that this particular picture has been seen.
add event_pic.get(x, y, proportional) xalign 0.5 yalign 0.0
## The gallery screen displays the gallery attached to a particular girl pack. It is called from the main menu.
screen gallery(name="", gal=None, bg=None): # The Gallery object must have a pics variable (a list of renpy displayables or image files)
tag menu
default page=0
key "mouseup_3" action Return()
key "K_ESCAPE" action Return()
add "#000"
add bg xalign 0.5 yalign 0.5
hbox:
vbox spacing 10:
text name
frame background None:
id "gallery"
xsize 1.0
ysize 0.9
has hbox box_wrap True
$ index = page*12
for i in xrange(12):
if index+i < len(gal.pics):
$ pic = gal.pics[index+i]
frame background None xsize 250 ysize 190:
if gal.renpy_images:
add gal.make_button(pic, ProportionalScale(ImageReference(pic), 240, 180), xalign=0.5) alpha 0.8
else:
add gal.make_button(pic, ProportionalScale(pic, 240, 180), xalign=0.5, background=c_cream) alpha 0.8
$ max_page = (len(gal.pics)-1) // 12
text "Page " + str(page+1) + "/" + str(max_page+1) size 14
hbox:
# textbutton "Start Slideshow" action gal.ToggleSlideshow()
if page > 0:
key "K_LEFT" action SetScreenVariable("page", page-1)
key "repeat_K_LEFT" action SetScreenVariable("page", page-1)
textbutton "Previous" action SetScreenVariable("page", page-1)
if page < max_page:
key "K_RIGHT" action SetScreenVariable("page", page+1)
key "repeat_K_RIGHT" action SetScreenVariable("page", page+1)
textbutton "Next" action SetScreenVariable("page", page+1)
key "K_HOME" action SetScreenVariable("page", 0)
key "K_END" action SetScreenVariable("page", max_page)
textbutton "Return" action Return()
## These two screens override the native ren'py screens
screen _gallery:
if locked:
add "#000"
text _("Image [index] of [count] locked.") align (0.5, 0.5)
else:
add "#000"
for d in displayables:
add d xalign 0.5 yalign 0.0
if gallery.slideshow:
timer gallery.slideshow_delay action Return("next") repeat True
key "game_menu" action gallery.Return()
if gallery.navigation:
use gallery_navigation
screen gallery_navigation:
hbox:
spacing 20
style_group "gallery"
align (.98, .98)
key "mouseup_3" action gallery.Return()
key "K_ESCAPE" action gallery.Return()
key "K_LEFT" action gallery.Previous(unlocked=True)
key "repeat_K_LEFT" action gallery.Previous(unlocked=True)
key 'K_BACKSPACE' action gallery.Previous(unlocked=True)
key "K_RIGHT" action gallery.Next(unlocked=True)
key "repeat_K_RIGHT" action gallery.Next(unlocked=True)
key 'K_SPACE' action gallery.Next(unlocked=True)
key "K_RETURN" action gallery.ToggleSlideshow()
key "K_SCROLLOCK" action gallery.ToggleSlideshow()
textbutton _("prev") action gallery.Previous(unlocked=True)
textbutton _("next") action gallery.Next(unlocked=True)
textbutton _("slideshow") action gallery.ToggleSlideshow()
textbutton _("return") action gallery.Return()
python:
style.gallery = Style(style.default)
style.gallery_button.background = None
style.gallery_button_text.color = "#666"
style.gallery_button_text.hover_color = "#fff"
style.gallery_button_text.selected_color = "#fff"
style.gallery_button_text.size = 16