Adding a history/backlog jump button

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
PixelatedRain
Newbie
Posts: 2
Joined: Thu Sep 08, 2022 8:23 pm
Contact:

Adding a history/backlog jump button

#1 Post by PixelatedRain »

It seems like most mainstream visual novels that don't use the Ren'Py engine have this feature so I'm surprised that there's not a good way I've found to do this, and I've searched and tinkered for many hours but with little results. This is a little hard to explain so please bear with me.

The main way I've seen floating around to add a history/backlog jump button is with this code, which rolls back to the identifier of the targeted HistoryEntry object in the _history_list variable:

Code: Select all

define gui.button_text_idle_color = '#00FF00'

screen history():

    tag menu

    ## Avoid predicting this screen, as it can be very large.
    predict False

    use game_menu(_("History"), scroll=("vpgrid" if gui.history_height else "viewport"), yinitial=1.0):

        style_prefix "history"

        for h in _history_list:

            window:

                ## This lays things out properly if history_height is None.
                has fixed:
                    yfit True

################## History/backlog jump button code.

                # Shows the number of checkpoints that Ren'Py thinks need to be rolled back to get to this line. If this is "None," it can't get here.
                $ getIdentifier = renpy.get_identifier_checkpoints(h.rollback_identifier)
                text "h.rollback_identifier: [getIdentifier]" xalign 0.0 yalign 0.3

                # The necessary history/backlog jump button.
                textbutton _("Jump") action RollbackToIdentifier(h.rollback_identifier)

################## End of extra code.

                if h.who:

                    label h.who:
                        style "history_name"
                        substitute False

                        ## Take the color of the who text from the Character, if
                        ## set.
                        if "color" in h.who_args:
                            text_color h.who_args["color"]

                $ what = renpy.filter_text_tags(h.what, allow=gui.history_allow_tags)
                text what:
                    substitute False

        if not _history_list:
            label _("The dialogue history is empty.")
Image

Because config.hard_rollback_limit = 100 by default, the max rollback stops here:

Image

config.hard_rollback_limit and config.rollback_length must be changed together. Because config.rollback_length = 128 by default, 128 it will always be the maximum number of rollbacks regardless of config.hard_rollback_limit. This is not a problem, as these both can be set to 250, to match config.history_length = 250 by default.

Here's where the problem begins. If I rollback 98 lines, it jumps there just fine, but then I can only rollback 2 more lines. Annoyingly, it does not load another 100 lines for rollback, the remainders stay as "None." You could try to hide this issue by setting the config.hard_rollback_limit and config.rollback_length variables to something like crazy like 1,000, but it would just make it really laggy, unoptimized, and this still doesn't fix the problem if the user, for example, does 10 rollbacks of 100 lines in a row:

Image

==============================================================================================================================================

So I came up with a new idea of forcing rolling back the same number of lines in the history as the number of checkpoints it would take to get there, the "Actual" number:

Code: Select all

[code]define gui.button_text_idle_color = '#00FF00'

screen history():

    tag menu

    ## Avoid predicting this screen, as it can be very large.
    predict False

    use game_menu(_("History"), scroll=("vpgrid" if gui.history_height else "viewport"), yinitial=1.0):

        style_prefix "history"

########## The number of objects resets to 0 each time the list is loaded.
        $ number = 0

        for h in _history_list:

            window:

                ## This lays things out properly if history_height is None.
                has fixed:
                    yfit True

################## History/backlog jump button code.

                # Shows the number of checkpoints that Ren'Py thinks need to be rolled back to get to this line.
                # If this is "None" it can't get here.
                $ getIdentifier = renpy.get_identifier_checkpoints(h.rollback_identifier)
                text "h.rollback_identifier: [getIdentifier]" xalign 0.0 yalign 0.3

                # Shows the actual number of checkpoints that Ren'Py needs to roll back to get to this line.
                # This list has to be reversed, or the numbers would be start large and get small.

                # Gets the total number of history objects until it maxes out at 250, which is the default.
                # This is necessary for any number of history objects under 250 at the beginning of the game.
                $ historynum = len(_history_list)
                # Adds 1 for each new history object.
                $ number += 1
                # Uses the total number of history objects and the current number to reverse the list.
                $ getActual = historynum + 1 - number
                text "Actual: [getActual]" xalign 0.0 yalign 0.6

                # The necessary history/backlog jump button, which now rolls back the exact number it should.
                textbutton _("Jump") action Rollback(force=True, checkpoints=getActual, abnormal=False)

################## End of extra code.

                if h.who:

                    label h.who:
                        style "history_name"
                        substitute False

                        ## Take the color of the who text from the Character, if
                        ## set.
                        if "color" in h.who_args:
                            text_color h.who_args["color"]

                $ what = renpy.filter_text_tags(h.what, allow=gui.history_allow_tags)
                text what:
                    substitute False

        if not _history_list:
            label _("The dialogue history is empty.")
Image

Image

This works, but for some reason I can't rollback to where the h.rollback_identifier and "Actual" number are not the same or is "None," even though I'm not using h.rollback_identifier to jump. And it still doesn't load another 100 lines for rollback, the remainders always stay as "None." The final images below show what I mean by checkpoints being of order. The identifier number should just count up normally like the "Actual" number I added, but sometimes, randomly through stress testing, loading and unloading, and refreshing, it will eventually be out of order and say "None" randomly:

Image

Image

The perfect solution would be getting the game to always rollback to the "Actual" number, without question, and without any errors, every time, because the "Actual" number is always correct. But I'm out of ideas. If anyone has a way to improve my code or something completely different to get this cool feature to work, please let me know. Thanks.

Here are the files of my test game if you're interested:
script.rpy
(6.08 KiB) Downloaded 19 times
screens.rpy
(41.48 KiB) Downloaded 18 times
And the traceback below when I try to jump and it doesn't work because the checkpoints are out of order or "None":

Code: Select all

I'm sorry, but an uncaught exception occurred.

While running game code:
  File "renpy/common/00gamemenu.rpy", line 170, in script
    $ ui.interact()
  File "renpy/common/00gamemenu.rpy", line 170, in <module>
    $ ui.interact()
  File "renpy/common/00action_other.rpy", line 233, in __call__
    renpy.rollback(*self.args, **self.kwargs)
Exception: Couldn't find a place to stop rolling back. Perhaps the script changed in an incompatible way?

-- Full Traceback ------------------------------------------------------------

Full traceback:
  File "renpy/common/00gamemenu.rpy", line 170, in script
    $ ui.interact()
  File "D:\Programs\renpy-8.0.3-sdk\renpy\ast.py", line 1131, in execute
    renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\python.py", line 1061, in py_exec_bytecode
    exec(bytecode, globals, locals)
  File "renpy/common/00gamemenu.rpy", line 170, in <module>
    $ ui.interact()
  File "D:\Programs\renpy-8.0.3-sdk\renpy\ui.py", line 299, in interact
    rv = renpy.game.interface.interact(roll_forward=roll_forward, **kwargs)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\core.py", line 3377, in interact
    repeat, rv = self.interact_core(preloads=preloads, trans_pause=trans_pause, pause=pause, pause_start=pause_start, pause_modal=pause_modal, **kwargs) # type: ignore
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\core.py", line 4258, in interact_core
    rv = root_widget.event(ev, x, y, 0)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\transition.py", line 53, in event
    return self.new_widget.event(ev, x, y, st) # E1101
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\screen.py", line 743, in event
    rv = self.child.event(ev, x, y, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1399, in event
    rv = super(Window, self).event(ev, x, y, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 279, in event
    rv = d.event(ev, x - xo, y - yo, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1399, in event
    rv = super(Window, self).event(ev, x, y, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 279, in event
    rv = d.event(ev, x - xo, y - yo, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 279, in event
    rv = d.event(ev, x - xo, y - yo, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\viewport.py", line 298, in event
    rv = super(Viewport, self).event(ev, x, y, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 279, in event
    rv = d.event(ev, x - xo, y - yo, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1399, in event
    rv = super(Window, self).event(ev, x, y, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 279, in event
    rv = d.event(ev, x - xo, y - yo, st)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\layout.py", line 1175, in event
    rv = i.event(ev, x - xo, y - yo, cst)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\behavior.py", line 1073, in event
    return handle_click(self.clicked)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\behavior.py", line 1008, in handle_click
    rv = run(action)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\display\behavior.py", line 329, in run
    return action(*args, **kwargs)
  File "renpy/common/00action_other.rpy", line 233, in __call__
    renpy.rollback(*self.args, **self.kwargs)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\exports.py", line 1716, in rollback
    renpy.game.log.rollback(checkpoints, greedy=greedy, label=label, force=(force is True), abnormal=abnormal, current_label=current_label)
  File "D:\Programs\renpy-8.0.3-sdk\renpy\rollback.py", line 889, in rollback
    self.load_failed()
  File "D:\Programs\renpy-8.0.3-sdk\renpy\rollback.py", line 807, in load_failed
    raise Exception("Couldn't find a place to stop rolling back. Perhaps the script changed in an incompatible way?")
Exception: Couldn't find a place to stop rolling back. Perhaps the script changed in an incompatible way?

Windows-10-10.0.19043 AMD64
Ren'Py 8.0.3.22090809
History Backlog Jump Button Test 803 1.0
Thu Sep  8 23:13:10 2022

User avatar
Ocelot
Lemma-Class Veteran
Posts: 2417
Joined: Tue Aug 23, 2016 10:35 am
Github: MiiNiPaa
Discord: MiiNiPaa#4384
Contact:

Re: Adding a history/backlog jump button

#2 Post by Ocelot »

Rollback works by saving whole game state at certain moments and keeping it in memory. Consider it as having 100 temporary saves. As soon as you going over the limit, oldest states are dropped from memory. This is why it doesn't load another rollback lines, because corresponding state is gone, reduced to atoms. Game doesn't know if, for example, at t-100 line you have 10 respect because you randomly got it at t-102 or you had it before, because now there is no information about it.
< < insert Rick Cook quote here > >

PixelatedRain
Newbie
Posts: 2
Joined: Thu Sep 08, 2022 8:23 pm
Contact:

Re: Adding a history/backlog jump button

#3 Post by PixelatedRain »

Ocelot wrote: Fri Sep 09, 2022 5:19 am Rollback works by saving whole game state at certain moments and keeping it in memory. Consider it as having 100 temporary saves. As soon as you going over the limit, oldest states are dropped from memory. This is why it doesn't load another rollback lines, because corresponding state is gone, reduced to atoms. Game doesn't know if, for example, at t-100 line you have 10 respect because you randomly got it at t-102 or you had it before, because now there is no information about it.
Thanks for the response. Then, other than using rollback, could there be another way? The "dismiss" keymap function, for example, which is clicking the left mouse button, hitting enter, etc., advances the text by one. Would it be possible to make the opposite of this function, then loop it the number of times it needs to until the desired line is reached? I couldn't find the code for it so I don't know how it works exactly. There's also the Return() action which advances the text too.

User avatar
Ocelot
Lemma-Class Veteran
Posts: 2417
Joined: Tue Aug 23, 2016 10:35 am
Github: MiiNiPaa
Discord: MiiNiPaa#4384
Contact:

Re: Adding a history/backlog jump button

#4 Post by Ocelot »

That is waht rollback is doing. Generally, without going through rollback: no. COnsider this line: $ stage = 5. After this line value of variable stagewill be 5. What should it be if we advance backward before this line? It is possible to know if you saved game state before this line (like rollback system does), and impossible if you didn't. This is price you have to pay for allowing creators do anything they want: it is impossible to know what they will do and you have to drop all assumptions on what they will do. If game engine restricts users to small subset of possible actions, then it would be possible to enumerate all of them.
< < insert Rick Cook quote here > >

Post Reply

Who is online

Users browsing this forum: Semrush [Bot]