refunct-tas
refunct-tas copied to clipboard
Savestates
With teleportbuttons used for example by spiral.lua, this requirement doesn't seem too urgent anymore.
There are currently 4 ideas:
- Record and Replay Demos. During Replay, if you want to continue from a state, try to continue playing from that point.
- Look at the source code of demo recording / playback. Reimplement important parts and try to create savestates based on that.
- Manually take a look at available Actors and other data, which will be saved between each frame to allow going back to any point.
- Improve the teleportbutton script to be as fast as possible and together with #18 try to get that approach to be very fast.
If replays are working, but continuing playing from a state inside the replay does not, it might be worth investigating the demo file format to use replays to record games and parse the replay to manually reset game values to the state of a frame inside the demo.
@stfx thanks a lot for your comments on the commit . Let's move that discussion over here as I'm going to force-push that commit soon :)
Adding that line with the preceding + to the config file didn't work. When I removed the +, the DemoNetDriver popped up in the list in-memory, but the SteamNetDriver was gone. For testing purposes I'm going with this approach, as I don't use the steam overlay anyway on Linux. But if this turns out to be working, I'm going to either have a look at the ini format UE uses to try to figure out a way to keep both drivers or just manually add the DemoNetDriver definition to the in-memory list when my library is injected.
As for the current status I seem to be able to successfully record demos. I'm not able to play them back yet, but I'm investigating it.
Current status: I'm able to start and stop recording replays, which create a demo file in the Refunct's savefolder. Replaying resets the game back to the Refunct logo on the black background, but while the logo fades as usual, there is no transition from the black screen to the game. Refunct stays black until I restart it.
Here are some observations:
-
While the replay is playing, buttons are triggered as recorded, I hear the sound of rising platforms at the time of the replay where I pressed the button.
-
There is no music, only sounds.
-
There are only platform rising sounds, no sounds regarding the character are played.
-
I can manually call
OpenLevel("WorldBase")to reset the game back to the Refunct logo on the black screen. This will reload the level, recreate all classes (and thus invalidate my previously acquired pointers) and the transition from black to the level works as usual. -
The camera position in-memory is
(-1125.0, -500.0, 0.0). -
Pressing keys does not change the camera position, nor does pressing Esc release the mouse.
-
AMyCharacterdoes not have anAPlayerControllerattached during or after the replay. According to the UE docs, it should have:The Game Mode will use a different Player Controller class (designated as ReplaySpectatorPlayerControllerClass) when viewing replays.
AGameMode.PlayerControllerClassis described as "The class of PlayerController to spawn for players logging in." I assume that theAMyCharacterclass is not recorded / replayed correctly / produces an error while being replayed, which may also result in no character-attached sounds being played. -
AMyCharacter::Tickis called for every frame during and after the replay. -
When the game SEGFAULTs (and I have gdb attached), the blackscreen turns to the actual game for that last frame as can be seen here:

-
If I record 500 frames and don't move, the replay ends up to be around 11.5kB in size, while changing my location and rotation during every frame increases the demo's filesize by 1.5kB. Theoretically, my location and rotation together should be 6
f32, which is 24 bytes per frame. Over 500 frames that would end up as 12kB, which is magnitudes larger than the actual file size difference. This may be due to compression or good delta conversions, or indicate thatAMyCharacteris not recorded correctly. -
If I call
OpenLevelwith any actual level that is not"WorldBase"(e.g."crouch1"), the screen just turns black, but without the fading Refunct logo. One theory might be that the replay first loads the"WorldBase", then tries to load the first streaming level, which ends up producing the black screen. -
During
PlayReplay, the functionUGameplayStatics::OpenLevelis never called. -
Both
PlayReplayandOpenLevelresult in new classes being created i.e. my previous pointers pointing to invalid data. -
IIRC (not sure right now)
AMyHUD::DrawHUDis not called on each tick during and after the replay, but is called every tick after callingOpenLevelwith a streaming level.
I'm going to implement #21 and #32 before continuing with this approach, which should enable me to display important debugging information on the HUD while I'm further investigating this behaviour.
But honestly at this point I think it's faster to try out approach 3 rather than continuing to investigate the behaviour of 1.
Which was my initial proposition, because I kinda assumed that the replay-system might have issues like that - it just isn't really built for savestates.
I know I'm not adding more than a "I told you so" with this comment, but the other approach has so many advantages that I'm not sure why you didn't go for it in the first place.
The main reason is that finding all data that needs to be saved is not an easy task. But with #32 I'm going to take a deeper look at a lot of data structures anyway, so I'm going to combine that.
But: I don't know if just setting the values to the saved ones is enough to reset the game into that state. For the position and rotation of the player I know this to be true. Setting the velocity and acceleration might not be that easy, though. Additionally, I don't know if it's enough for me to set the level variable for the game to load / unload the streaming levels. In fact I assume that stfx implemented the level variable just as counter for the autosplitter and only writes to it. So I'll most likely need to take a look at streaming levels and manually reset the loaded levels.
Regarding Level Streaming:
- Loading streaming level
- Unloading streaming level
- Level loading transition
- Refunct most likely uses
ULevelStreamingAlwaysLoaded
Refunct does not reload the level after the initial game launch and instead manually reverts all states to default to be able to have instant restarts. So while tracking the state of all actors and the game instance should result in something similar to what you are looking for there are times especially during any animations like player climbing, player travelling in a pipe, lift moving, island rising and so on where it would be extremely challenging to get this to work right. Therefore I would only recommend idea number 4 mentioned here.
New Idea: Refunct is creating and loading save files. I could use that to manually create a save file, which I then load, which loads the correct level state, allowing me to skip the teleportbuttons, which should save some time when using "savestates". That way I can restore the game to the correct level instantaneously, which I can finish up with just setting the position / rotation.
That's still not real savestates, but should be helpful for the practice tool for the time being.
With the latest implementation we control the level and some other state which makes the previous teleportbutton approach obsolete. Instead, when "New Game" is pressed with a practice state active, the level is set to what it should be, the player is teleported and pawns are spawned on all buttons that should be pressed. We need to wait 4.5 seconds (9 frames at 0.5s each) for all platforms to be raised initially such that all pawns can activate all the buttons. Then we need to wait another 4.5 seconds for all platforms to "finish their superjump" (even if a cluster is raised already, if the button that should raise it is pressed, you can superjump from the raised platforms while the cluster would be rising). This leaves us with 18 total frames required, which is less than a third of a second at 60 FPS.
However, some other form of savestates are still needed for proper TASing, although that's only required if we ever find a fix for #92.
The way I'm thinking of doing this is by having a SaveState struct and tracking, saving and restoring every value within this struct.
An example SaveState struct is:
struct SaveState {
is_jump_down: bool,
is_crouch_down: bool,
is_in_water_volume: bool,
is_diving: bool,
is_underwater: bool,
is_in_water: bool,
base_speed: f32,
bonus_speed: f32,
max_bonus_speed: f32,
bonus_interp_speed: f32,
movement_mode: u8,
pending_launch_velocity: bool,
is_in_pipe: bool,
is_on_springpad: bool,
is_on_lift: bool,
pipe_progress: f32,
pipe_direction: u8,
springpad_press_progress: f32,
springpad_launch_progress: f32,
lift_progress: f32,
lift_direction: u8,
player_location: FVector,
player_rotation: FRotator,
player_velocity: FVector,
player_acceleration: FVector,
current_button: FVector,
current_level: FVector,
current_cube: FVector,
current_platform: FVector,
pressed_buttons: Vec<ObjectWrapper>,
collected_cubes: Vec<ObjectWrapper>,
colored_platforms: Vec<ObjectWrapper>,
risen_clusters: Vec<ObjectWrapper>,
keys_pressed: Set<int>,
is_climbing: bool,
climbing_progress: f32,
}