Ren'Py Programmatically saving to next available slot, with thumbnail

icytease

Newbie
Apr 5, 2024
22
15
13
hihi, so I decided to try making a personal mod that I can plop into an AVN's game folder to add two keyboard shortcuts for myself:
  • A - toggles auto-forward, with a little message
  • P - makes a normal save in the next available slot
After some googling and testing, I got it to the point where both keys seem to be functioning, but with a thumbnail issue on save. From what I could understand, RenPy does the thumbnail screenshot for you, usually, but I'm having trouble figuring out what commands I'd need to add, because my thumbnail is always some odd splash(?) image instead of the game's current image.

Python:
init -999 python:
    config.developer = True
    config.console = True
    is_forwarding = False
    
    def toggle_forwarding():
        """
        Toggles Auto-Forward with a little notification in top-left
        """
        global is_forwarding
        is_forwarding = not is_forwarding
        if is_forwarding:
            renpy.notify("Auto-forwarding!")
            renpy.run(Preference("auto-forward", "enable"))
        else:
            renpy.notify("Back to Normal")
            renpy.run(Preference("auto-forward", "disable"))
    
    def save_in_next_available_slot(save_name_string=""):
        """
        Finds the first empty save slot and saves the game to it.
        Looks like 3-6 , where 3 is the page and 6 is the slot
        """
        slots_per_page = gui.file_slot_cols * gui.file_slot_rows
        for page in range(1, 101):
            for slot in range(1, slots_per_page+1):
                slot_name = f"{page}-{slot}"
                if not renpy.can_load(slot_name):
                    renpy.save(slot_name, save_name_string)
                    renpy.notify(f"Game saved to slot {slot_name}")
                    return
        renpy.notify("No empty save slots found in the range 1-100.")
    
    # Assign "a" to auto-forward and "p" to saving in next slot
    try:
        if 'K_a' in config.keymap['game_menu']:
            config.keymap['game_menu'].remove('K_a')
        if 'K_p' in config.keymap['game_menu']:
            config.keymap['game_menu'].remove('K_p')
    except:
        pass
    config.underlay.append(renpy.Keymap(K_a=toggle_forwarding))
    config.underlay.append(renpy.Keymap(K_p=save_in_next_available_slot))
 

osanaiko

Engaged Member
Modder
Jul 4, 2017
3,437
6,603
707
FileTakeScreenshot ( ) action seems to what you need.

Try adding a renpy.file_take_screenshot() just before your renpy.save()
 
  • Like
Reactions: icytease

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,933
21,499
1,026
Python:
    def toggle_forwarding():
        """
        Toggles Auto-Forward with a little notification in top-left
        """
        global is_forwarding
        is_forwarding = not is_forwarding
        if is_forwarding:
            renpy.notify("Auto-forwarding!")
            renpy.run(Preference("auto-forward", "enable"))
        else:
            renpy.notify("Back to Normal")
            renpy.run(Preference("auto-forward", "disable"))
There's no need for the global, variables in the game scope are always global to the function declared in a RPY file.

"is_forwarding" is not necessary, the auto-forward state should be stored in preferences.afm_enable. Therefore this should be enough:
Python:
    def toggle_forwarding():
        """  Toggles Auto-Forward with a little notification in top-left """
        if preferences.afm_enable:
            renpy.notify("Back to Normal")
        else:
            renpy.notify("Auto-forwarding!")
        preferences.afm_enable = not preferences.afm_enable

FileTakeScreenshot ( ) action seems to what you need.

Try adding a renpy.file_take_screenshot() just before your renpy.save()
It's the right answer, but not the right function. It's name is in fact renpy.take_screenshot().
 

icytease

Newbie
Apr 5, 2024
22
15
13
There's no need for the global, variables in the game scope are always global to the function declared in a RPY file.
Hmmm, not sure what you mean here, as I had declared is_forwarding in a different scope and was modifying it in the local scope. Would've errored and complained without that syntax.

"is_forwarding" is not necessary, the auto-forward state should be stored in preferences.afm_enable. Therefore this should be enough:
Python:
    def toggle_forwarding():
        """  Toggles Auto-Forward with a little notification in top-left """
        if preferences.afm_enable:
            renpy.notify("Back to Normal")
        else:
            renpy.notify("Auto-forwarding!")
        preferences.afm_enable = not preferences.afm_enable
This is a nice find to clean it up, for sure! Thanks! It's awesome that assigning preferences.afm_enable actually makes it happen, too! I went with the toggle first, and the notify after.

As for the unintuitive (to me, lol) thumbnail function, yep, ty osanaiko and anne O'nymous! With changes, I now have:

Python:
init -999 python:
    config.developer = True
    config.console = True
    
    def toggle_forwarding():
        """
        Toggles Auto-Forward with a little notification in top-left
        """
        preferences.afm_enable = not preferences.afm_enable
        if preferences.afm_enable:
            renpy.notify("Auto-forwarding!")
        else:
            renpy.notify("Back to Normal")
        
    def save_in_next_available_slot(save_name_string=""):
        """
        Finds the first empty save slot and saves the game to it.
        Looks like 3-6 , where 3 is the page and 6 is the slot
        """
        slots_per_page = gui.file_slot_cols * gui.file_slot_rows
        for page in range(1, 101):
            for slot in range(1, slots_per_page+1):
                slot_name = f"{page}-{slot}"
                if not renpy.can_load(slot_name):
                    renpy.take_screenshot()  # Thumbnail
                    renpy.save(slot_name, save_name_string)
                    renpy.notify(f"Game saved to slot {slot_name}")
                    return
        renpy.notify("No empty save slots found in the first hundred pages.")
    
    # Assign "a" to auto-forward and "p" to saving in next slot
    try:
        if 'K_a' in config.keymap['game_menu']:
            config.keymap['game_menu'].remove('K_a')
        if 'K_p' in config.keymap['game_menu']:
            config.keymap['game_menu'].remove('K_p')
    except:
        pass
    config.underlay.append(renpy.Keymap(K_a=toggle_forwarding))
    config.underlay.append(renpy.Keymap(K_p=save_in_next_available_slot))
 
  • Heart
Reactions: osanaiko

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,933
21,499
1,026
Hmmm, not sure what you mean here, as I had declared is_forwarding in a different scope and was modifying it in the local scope. Would've errored and complained without that syntax.
No, it wouldn't complain, try it, you'll see.
And unconsciously you already know it. Look at your original code. You use renpy.notify and renpy.run. There's no global renpy to tell Python to import the "renpy" variable into the function scope, and yet you can address the module stored in this variable and access its functions.

What would happen is that, due to Python immutability, assigning a value directly to "is_forwarding" would create a local variable at this name. But if you assign it a value indirectly, with store.is_forwarding = not is_forwarding, it works perfectly and propagate the change outside of the local scope.
 

icytease

Newbie
Apr 5, 2024
22
15
13
No, it wouldn't complain, try it, you'll see.
And unconsciously you already know it. Look at your original code. You use renpy.notify and renpy.run. There's no global renpy to tell Python to import the "renpy" variable into the function scope, and yet you can address the module stored in this variable and access its functions.

What would happen is that, due to Python immutability, assigning a value directly to "is_forwarding" would create a local variable at this name. But if you assign it a value indirectly, with store.is_forwarding = not is_forwarding, it works perfectly and propagate the change outside of the local scope.
Oh, thanks, I was not aware of the store umbrella, which definitely changes things. I did test it before my reply as I had written it -- without the global line, an error with unreferenced variable, as expected. I see that the RenPy framework made the store for these kinds of conveniences in mind!
 

eevkyi

Member
Aug 14, 2025
316
345
83
In addition to what has already been mentioned, I suggest avoiding f-strings to improve compatibility with older games. I also recommend restricting its usage in the main menu, including some error handling, and setting another key to open the accessibility menu when available:
Python:
init -999 python:
    config.developer = True
    config.console = True

    def toggle_forwarding():
        """
        Toggles Auto-Forward with a little notification in top-left.
        """
        if main_menu: # Edit: Using main_menu directly is more reliable in this approach.
            renpy.notify("Auto-forwarding isn't available in main menu.")
            return

        preferences.afm_enable = not preferences.afm_enable
        try:
            if preferences.afm_enable:
                renpy.notify("Auto-forwarding enabled!")
            else:
                renpy.notify("Auto-forwarding disabled")
        except Exception as e:
            renpy.notify("Error toggling auto-forward: {}".format(str(e)))

    def save_in_next_available_slot(save_name_string=""):
        """
        Finds the first empty save slot and saves the game to it.
        Looks like 3-6 , where 3 is the page and 6 is the slot.
        """
        if main_menu: # Edit: Using main_menu directly is more reliable in this approach.
            renpy.notify("Saving isn't available in main menu.")
            return

        try:
            slots_per_page = gui.file_slot_cols * gui.file_slot_rows
            for page in range(1, 101):
                for slot in range(1, slots_per_page+1):
                    slot_name = "{}-{}".format(page, slot)
                    if not renpy.can_load(slot_name):
                        renpy.take_screenshot()
                        renpy.save(slot_name, save_name_string)
                        renpy.notify("Game saved to slot {}".format(slot_name))
                        return
            renpy.notify("No empty save slots found in the range 1-100.")
        except Exception as e:
            renpy.notify("Save error: {}".format(str(e)))

    def accessibility_menu_fallback():
        """
        Toggles accessibility menu when available.
        """
        try:
            renpy.run(ToggleScreen("_accessibility"))
        except:
            renpy.notify("Accessibility menu not available.")

    # Assign "a" to auto-forward, "p" to saving in next slot and "z" to toogle accessibility menu.
    try:
        for key in ['K_a', 'K_p', 'K_z']:
            if key in config.keymap.get('game_menu', []):
                config.keymap['game_menu'].remove(key)
    except (KeyError, ValueError):
        pass

    config.underlay.append(renpy.Keymap(K_a=toggle_forwarding))
    config.underlay.append(renpy.Keymap(K_p=save_in_next_available_slot))
    config.underlay.append(renpy.Keymap(K_z=accessibility_menu_fallback))
Restricting usage in the main menu will prevent certain problems, such as broken saves. Handling errors from the start will make your life easier if you continue expanding this mod.

Reducing the number of pages might be advisable to avoid performance issues in certain scenarios, but it may not make a difference on your device.
 
Last edited:
  • Like
Reactions: anne O'nymous

icytease

Newbie
Apr 5, 2024
22
15
13
In addition to what has already been mentioned, I suggest avoid using f-strings to improve compatible with older games...
I see the concern but I love f-strings too much :3 Probably one of my fav parts, tbh! Plus they've been around for almost 10 years! And you can probably swap out Pythons if the game's lib is too old, right?
AI Overview
Python f-strings (formatted string literals) were introduced in 2016 with the release of Python 3.6. They were added as part of PEP 498 to provide a more concise and readable way to embed expressions within string literals.
Restricting usage in the main menu will prevent certain problems, such as broken saves. Handling errors from the start will make your life easier if you continue expanding this mod.
Yeah, this could be a nice way to keep it defensive.

Reducing the number of pages might be advisable to avoid performance issues in certain scenarios, but it may not make a difference on your device.
Mhmm, probably negligible. I could track furthest save and begin iterating from there, for the subsequent presses of "P" within that session, instead of starting at "1-1" every time.

I don't personally make use of the accessibility menu, but definitely feel free to use this as a base!
 
  • Like
Reactions: eevkyi

eevkyi

Member
Aug 14, 2025
316
345
83
I see the concern but I love f-strings too much :3 Probably one of my fav parts, tbh! Plus they've been around for almost 10 years! And you can probably swap out Pythons if the game's lib is too old, right?
I like it too, I find it more readable. Unfortunately, simply changing the python version from 2 to 3 or vice versa would add numerous incompatibilities.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,933
21,499
1,026
[...] I also recommend restricting its usage in the main menu, [...]
Python:
        if renpy.get_screen("main_menu"):
This can be avoided by relying on screen statement instead of renpy.Keymap objects like currently.

In place of the config.underlay.append(renpy.Keymap(KEY=FUNCTION)) lines:
Python:
init python:
    config.overlay_screens.append( "myKeys" )

screen myKeys():

    key "K_a" action Function( toggle_forwarding )
    [The other defined keys]
Overlays are automatically removed by Ren'Py when you enter the main menu, then restored when you leave it.


Ideally it should also scan the keys currently used, in order to avoid duplicate. This should do it:
Python:
init python:
    # Will return True is the key is not used in keymap, False else.
    keyIsFree = lambda key: not key in [ k for a in config.keymap for k in config.keymap[a] if config.keymap[a] ]
 
  • Like
Reactions: eevkyi