22-08-2026, 01:15 AM
Hi!
To preface this I would like to offer an explanation of what ReplayControl actually does and what it does not do.
I understand that some of what I wrote may not be easily understood from the outside (even from the inside it's a little confusing!) so let me explain. The advance() function is ReplayControl's own accumulator. One tick (in seconds) is passed into the function. From there, after checking if ReplayControl's pause is active and a check to see if the replay is finished, it adds the product of 1 tick and the rate multiplier to the head of the playback. This function's caller is WorldStatus::updateTime(). advance() runs and it's return value is given to setTime(), thus making m_head the source of truth when ReplayControl is enabled.
Essentially, when ReplayControl is active, instead of incrementing the clock normally, it goes through advance() instead. The way setTime() works is that it truncates to ticks and then feeds that same truncated tick count back to m_time. This is a non-issue under normal circumstances but under rate multipliers this inaccuracy quickly adds up, hence the need to keep m_head as an exact value and the need for ReplayControl to have it's own accumulator.
To tie it back to the world clock, moving m_head also moves the clock and changing the rate changes how much time is added to the world clock by means of multiplying the time delta. It writes to the same m_time and m_time_ticks that get called by anything that needs the world's time, such as a ghost kart. It does not change the world's stepping, 120 physics ticks pass
no matter what ReplayControl does, leading the desync with the world that you mentioned when the rate changes or when ReplayControl enables it's pause.
To answer your question: it does, however that's only part of the picture. It drives the clock, not the world, and I think that's where the discrepancy between what I've built and the future vision of the feature lies.
You outlined that pausing and changing the rate of playback via the world would be simple and I agree, the only slight challenge would be rewiring what I already have but even that's simple enough. Reversal of playback direction (rewinding) is another beast entirely though and it's the biggest task here by far.
Sadly there's no easy approach to it. Smooth and continuous rewind would most likely involve restoring to a snapshot every single frame and a rewind system like the one in multiplayer faces the risk of incurring noticable memory/performance problems in relation to snapshot frequency. Too often and your memory usage explodes, too sparse (say 2 seconds) and you cram 240 ticks worth of physics calculations into one frame which, as you can imagine, would cause a stutter. Perhaps an edit to the replay format would help a bit but that's beyond my scope and certainly a decision that cannot be made by myself (and, as you pointed out, on 1.5 we don't have the freedom to edit it). I could go on about the difficulties this poses but I think you get the gist.
Fortunately for us both, while I was waiting for a reply I built a prototype control interface for ReplayControl. As of writing it's only been a couple of hours since I finished it. No doubt there are a few style nits in the code itself but it's pretty clean and it works as intended so I think for a demo it'll be fine. If you want to give it a shot, here's the link:
https://github.com/schrowd/stk-code/tree...control-ui
For convenience, the commit message is here:
https://github.com/schrowd/stk-code/comm...223803aa68
Build it and then to enable it go to Singleplayer -> Ghost Replay Race -> [any replay] and then tick "Watch replay only" and then the "Enable replay control" box that appears. Then just start the race. The overlay then lists out the control scheme (This uses your normal race binds, not any artist debug binds. At the time of writing I elected to go with the normal ones for simplicity's sake), the current rate, the head of playback and the replay's duration.
This whole thing was built on the 1.5 branch and not the Evolution one as I wasn't aware that that was the ideal branch to work on. I'll be working on Evolution from now on.
On PR #5837, I fixed the indentation plus some tab indenting that was still there.
In regards to the code contribution guide, I think it covers pretty much everything you'd need to know in regards to contributing. Perhaps for the "Coding Style" section you could give a side-by-side example of code that fits the style and code that doesn't? We see code snippets in the "Layout" section yet the one titled "Coding" is a big text wall. I think some examples might help new contributors better understand the STK conventions.
To preface this I would like to offer an explanation of what ReplayControl actually does and what it does not do.
I understand that some of what I wrote may not be easily understood from the outside (even from the inside it's a little confusing!) so let me explain. The advance() function is ReplayControl's own accumulator. One tick (in seconds) is passed into the function. From there, after checking if ReplayControl's pause is active and a check to see if the replay is finished, it adds the product of 1 tick and the rate multiplier to the head of the playback. This function's caller is WorldStatus::updateTime(). advance() runs and it's return value is given to setTime(), thus making m_head the source of truth when ReplayControl is enabled.
Essentially, when ReplayControl is active, instead of incrementing the clock normally, it goes through advance() instead. The way setTime() works is that it truncates to ticks and then feeds that same truncated tick count back to m_time. This is a non-issue under normal circumstances but under rate multipliers this inaccuracy quickly adds up, hence the need to keep m_head as an exact value and the need for ReplayControl to have it's own accumulator.
To tie it back to the world clock, moving m_head also moves the clock and changing the rate changes how much time is added to the world clock by means of multiplying the time delta. It writes to the same m_time and m_time_ticks that get called by anything that needs the world's time, such as a ghost kart. It does not change the world's stepping, 120 physics ticks pass
no matter what ReplayControl does, leading the desync with the world that you mentioned when the rate changes or when ReplayControl enables it's pause.
Alayan Wrote:I got quite confused when reading your message and looking at your code on whether or not your change is driving the main world clock.
To answer your question: it does, however that's only part of the picture. It drives the clock, not the world, and I think that's where the discrepancy between what I've built and the future vision of the feature lies.
You outlined that pausing and changing the rate of playback via the world would be simple and I agree, the only slight challenge would be rewiring what I already have but even that's simple enough. Reversal of playback direction (rewinding) is another beast entirely though and it's the biggest task here by far.
Sadly there's no easy approach to it. Smooth and continuous rewind would most likely involve restoring to a snapshot every single frame and a rewind system like the one in multiplayer faces the risk of incurring noticable memory/performance problems in relation to snapshot frequency. Too often and your memory usage explodes, too sparse (say 2 seconds) and you cram 240 ticks worth of physics calculations into one frame which, as you can imagine, would cause a stutter. Perhaps an edit to the replay format would help a bit but that's beyond my scope and certainly a decision that cannot be made by myself (and, as you pointed out, on 1.5 we don't have the freedom to edit it). I could go on about the difficulties this poses but I think you get the gist.
Fortunately for us both, while I was waiting for a reply I built a prototype control interface for ReplayControl. As of writing it's only been a couple of hours since I finished it. No doubt there are a few style nits in the code itself but it's pretty clean and it works as intended so I think for a demo it'll be fine. If you want to give it a shot, here's the link:
https://github.com/schrowd/stk-code/tree...control-ui
For convenience, the commit message is here:
https://github.com/schrowd/stk-code/comm...223803aa68
Build it and then to enable it go to Singleplayer -> Ghost Replay Race -> [any replay] and then tick "Watch replay only" and then the "Enable replay control" box that appears. Then just start the race. The overlay then lists out the control scheme (This uses your normal race binds, not any artist debug binds. At the time of writing I elected to go with the normal ones for simplicity's sake), the current rate, the head of playback and the replay's duration.
This whole thing was built on the 1.5 branch and not the Evolution one as I wasn't aware that that was the ideal branch to work on. I'll be working on Evolution from now on.
On PR #5837, I fixed the indentation plus some tab indenting that was still there.
In regards to the code contribution guide, I think it covers pretty much everything you'd need to know in regards to contributing. Perhaps for the "Coding Style" section you could give a side-by-side example of code that fits the style and code that doesn't? We see code snippets in the "Layout" section yet the one titled "Coding" is a big text wall. I think some examples might help new contributors better understand the STK conventions.

