Asynchronous Replay System Not Working

Hey,
Attached below is a video of the problem:

The “synchronous playback” is the playback right after the replay is recorded and the “asynchronous playback” is the playback retrieved from persistent storage (datastore, stringvalue, etc.)

In the video, the synchronous replay works as expected; but when the player clicks on the asynchronous replay button (the button that says play), it just skips to the end.

The script is based off of @boatbomber’s VPF Replay Module:
VPF Replay Module - Resources / Community Resources - Developer Forum | Roblox

Here is the script:
local ipairs,pairs = ipairs,pairs
local UIS = game:GetService("UserInputService")
local RS = game:GetService("RunService")
local Mouse = game:GetService("Players").LocalPlayer:GetMouse()
local https = game:GetService("HttpService")

local DEBUG = true --RS:IsStudio()
local Module = {}
local RenderStepped = RS.RenderStepped
local serializer = require(script.Parent.RBXSerialize)

local function splitString(str, sep)
	local result = {}
	local index = 1
	while true do
		local start, finish = string.find(str, sep, index)
		if not start then
			break
		end
		local part = string.sub(str, index, start - 1)
		table.insert(result, part)
		index = finish + 1
	end
	local part = string.sub(str, index)
	table.insert(result, part)
	return result
end

local function Serialize(frames)
	return https:JSONEncode(frames)
end

local function Deserialize(data)
	return https:JSONDecode(data)
end

local CleanChildren = {["Decal"] = true;["Texture"] = true;["SpecialMesh"] = true;["BlockMesh"] = true;}
local function CleanClone(Object,CharClone)
	for i,v in pairs(game.ReplicatedStorage.Assets:GetDescendants()) do
		if v:IsA("BasePart") then
			if v.Name == Object.Name then
				local CleanObject = v:Clone()

				for _, c in ipairs(CleanObject:GetChildren()) do
					if not CleanChildren[c.ClassName] then
						c:Destroy()
					end
				end

				CleanObject.Anchored = true
				CleanObject.CanCollide = false
				CleanObject.CanQuery = false
				CleanObject.CanTouch = false
				CleanObject.CFrame = CleanObject.CFrame+Vector3.new(150,-200,0)
				CleanObject.Parent = CharClone

				return CleanObject
			end
		end
	end
end

function Module.new(Settings)

	Settings = Settings or {}

	local RecordingStopped = Instance.new("BindableEvent")
	local RecordingStarted = Instance.new("BindableEvent")
	local RegistrationCompleted = Instance.new("BindableEvent")
	local FrameChanged = Instance.new("BindableEvent")

	local Replay = {
		-- States
		Playing = false;
		Recording = false;
		Recorded = false;

		-- Main
		Registers = {};
		StaticRegisters = {};
		CloneIndex = {};
		RegisteredObjects = {};
		NameRegister = {};

		Frames = {};
		FrameTimes = {};
		FrameCount = 0;
		RecordingTime = 0;

		LastSnapshotTick = 0;
		FPSDelay = 1/(Settings.FPS or 30);

		-- Objects
		VPF = Instance.new("Folder");

		-- Connections (written for reference and autofill)
		RecordConnection = nil;
		PlayConnection = nil;

		-- Events
		RecordingStarted = RecordingStarted.Event;
		RecordingStopped = RecordingStopped.Event;
		RegistrationCompleted = RegistrationCompleted.Event;
		FrameChanged = FrameChanged.Event;


	}

	-- Setup
	Replay.VPF.Name = "ReplayFolder"
	Replay.VPF.Parent = workspace

	-- Primary functions

	function Replay:Register(Object, IgnoreDescendants)
		task.defer(function() -- Run async due to possible yielding

			if Replay.Recording then
				warn("Cannot register new objects while recording is in progress")
				return
			end

			if typeof(Object) ~= "Instance" then return end

			if DEBUG then
				print("Register:",Object)
			end
			
			table.insert(Replay.NameRegister,Object.Name)

			local IsHumanoid = false
			if Object:IsA("Model") and Object:FindFirstChildWhichIsA("Humanoid") then
				IsHumanoid = true
				if DEBUG then
					print("Character detected:",Object)
				end

				while Object:FindFirstChildWhichIsA("BasePart") == nil do
					task.wait(0.05) -- .CharacterAdded is fired before the character loads, so this wait is needed or the model has no basepart children yet
				end

				local CharClone = Instance.new("Model")
				CharClone.Name = Object.Name

				for _,Child in ipairs(Object:GetDescendants()) do
					if Child:IsA("BasePart") then
						if not Replay.RegisteredObjects[Child] then -- Avoid duplication
							if DEBUG then
								print("   Valid character part:",Child)
							end
							Replay.RegisteredObjects[Child] = true
							local Clone = CleanClone(Child,CharClone)
							Replay.CloneIndex[Child] = Clone
							Replay.Registers[#Replay.Registers+1] = {Mirror = Clone; Original = Child;}
						end
					elseif Child:IsA("CharacterMesh") then
						Child:Clone().Parent = CharClone
					end
				end

				local Shirt,Pants,Tee = Object:FindFirstChildWhichIsA("Shirt"),Object:FindFirstChildWhichIsA("Pants"),Object:FindFirstChildWhichIsA("ShirtGraphic")
				do -- Handles clothing in a `do end` just for easy code folding, not really about scope or anything
					if Shirt then
						if DEBUG then
							print("   Shirt registered:")
						end

						local ShirtClone = Shirt:Clone()
						ShirtClone.Parent = CharClone

						Shirt.Changed:Connect(function(Prop)
							ShirtClone[Prop] = Shirt[Prop]
						end)
					end

					if Pants then
						if DEBUG then
							print("   Pants registered:")
						end

						local PantsClone = Pants:Clone()
						PantsClone.Parent = CharClone

						Pants.Changed:Connect(function(Prop)
							PantsClone[Prop] = Pants[Prop]
						end)
					end

					if Tee then
						if DEBUG then
							print("   Tee registered:")
						end

						local TeeClone = Tee:Clone()
						TeeClone.Parent = CharClone

						Tee.Changed:Connect(function(Prop)
							TeeClone[Prop] = Tee[Prop]
						end)
					end
				end

				local StatelessHumanoid = Instance.new("Humanoid")
				do -- Again, the `do end` block is just so I can fold the code
					StatelessHumanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
					for _, enum in next, Enum.HumanoidStateType:GetEnumItems() do
						if (enum ~= Enum.HumanoidStateType.None) then
							StatelessHumanoid:SetStateEnabled(enum, false)
						end
					end
					StatelessHumanoid:SetStateEnabled(Enum.HumanoidStateType.RunningNoPhysics,true)

					StatelessHumanoid.RigType = Object:FindFirstChildWhichIsA("Humanoid").RigType
				end
				StatelessHumanoid.Parent = CharClone

				CharClone.Parent = Replay.VPF
			end

			if Object:IsA("BasePart") and Object.ClassName ~= "Terrain" and Object.Archivable then
				if not Replay.RegisteredObjects[Object] then -- Avoid duplication

					if DEBUG then
						print("   Valid register")
					end

					Replay.RegisteredObjects[Object] = true

					local Clone = CleanClone(Object)
					Replay.CloneIndex[Object] = Clone

					Clone.CFrame = Clone.CFrame+Vector3.new(150,-200,0)

					Replay.Registers[#Replay.Registers+1] = {Mirror = Clone; Original = Object;}
					Clone.Parent = Replay.VPF
				end
			end

			if not IsHumanoid then
				if not IgnoreDescendants then
					for _,Child in ipairs(Object:GetChildren()) do
						Replay:Register(Child)
					end
				end
			end

			RegistrationCompleted:Fire()
		end)
	end

	function Replay:StartRecording(MaxRecordingTime)
		-- Check if there is a previous recording, if yes, warn and exit the function
		if Replay.Recorded then
			warn("Cannot start recording until previous recording is cleared")
			return
		end

		-- Check if recording is already in progress, if yes, warn and exit the function
		if Replay.Recording then
			warn("Cannot start recording since recording is already in progress")
			return
		end

		-- Set the maximum recording time to 300000 milliseconds (5 minutes) if not provided
		MaxRecordingTime = MaxRecordingTime or 300000

		-- Print "Start Recording" if DEBUG is true (used for debugging purposes)
		if DEBUG then
			print("Start Recording")
		end

		-- Fire the RecordingStarted event to signal the start of recording
		RecordingStarted:Fire()

		-- Set the Recording flag to true, indicating that recording is in progress
		Replay.Recording = true

		-- Create a connection to the RenderStepped event to capture frame data
		Replay.RecordConnection = RenderStepped:Connect(function(DeltaTime)
			-- If the recording time exceeds the maximum recording time, stop recording
			if Replay.RecordingTime + DeltaTime > MaxRecordingTime then
				Replay:StopRecording()
				return
			end

			-- Update the recording time with the time since the last frame
			Replay.RecordingTime = Replay.RecordingTime + DeltaTime

			-- Calculate the time difference between the current frame and the last snapshot
			local FrameDelta = tick() - Replay.LastSnapshotTick

			-- Check if enough time has elapsed to take a new snapshot
			if FrameDelta >= Replay.FPSDelay then
				if DEBUG then
					print("Snapshotting")
				end

				-- Increment the frame count
				Replay.FrameCount = Replay.FrameCount + 1

				-- Update the last snapshot tick to the current tick
				Replay.LastSnapshotTick = tick()

				-- Store the recording time for this frame
				Replay.FrameTimes[Replay.FrameCount] = Replay.RecordingTime

				-- Create a table to store the data of each registered object for this frame
				local ObjectData = {}

				-- Loop through the registered objects and store their data
				for _, Object in ipairs(Replay.Registers) do
					local mir, og = Object.Mirror, Object.Original
					if og and og:IsDescendantOf(game) then
						if og.ClassName == "ParticleEmitter" then
							-- For ParticleEmitter, we currently do nothing (can be customized)
						else
							-- Store the CFrame, Color, and Transparency of the object for this frame
							ObjectData[Object.Mirror] = {
								["CFrame"] = serializer.Encode(og.CFrame);
								["Color"] = serializer.Encode(og.Color);
								["Transparency"] = og.Transparency;
							}
						end
					else
						-- If the object is not present in the game, mark it as destroyed
						ObjectData[Object.Mirror] = {
							["Destroyed"] = true;
						}
					end
				end

				-- Set the frame length for the previous frame (the time between snapshots)
				if Replay.Frames[Replay.FrameCount - 1] then
					Replay.Frames[Replay.FrameCount - 1].FrameLength = FrameDelta
				end

				-- Store the frame data for this frame
				Replay.Frames[Replay.FrameCount] = {
					ID = Replay.FrameCount;
					Time = Replay.RecordingTime;
					CameraCF = workspace.CurrentCamera.CFrame;
					ObjectData = ObjectData;
				}
			end
		end)
	end


	function Replay:StopRecording(calledByScript)
		if Replay.Recorded then
			warn("Cannot stop recording since it has already stopped")
			return
		end

		if not Replay.Recording then
			warn("Cannot stop recording since no recording is in progress")
			return
		end

		if DEBUG then
			print("Stop Recording")
		end

		Replay.RecordConnection:Disconnect()

		Replay.Recording = false
		Replay.Recorded = true

		RecordingStopped:Fire()
		
		local ObjectNames = {}
		for _, Object in ipairs(Replay.NameRegister) do
			table.insert(ObjectNames, Object)
		end
		
		local framesData = Serialize(ObjectNames).."UNIQUESEPERATOR"..Serialize(Replay.Frames)
		script.Parent.ServerFrames.Value = tostring(framesData)
	end

	function Replay:ClearRecording()

		if not Replay.Recorded then
			warn("Cannot clear nonexistent recording")
			return
		end

		if Replay.RecordConnection then
			Replay.RecordConnection:Disconnect()
		end
		if Replay.PlayConnection then
			Replay.PlayConnection:Disconnect()
		end

		Replay.FrameCount = 0
		Replay.Frames = {}
		Replay.FrameTimes = {}
		Replay.RecordingTime = 0

		Replay.Recorded = false
		Replay.Recording = false
		Replay.Playing = false

	end

	function Replay:Destroy()
		RecordingStopped:Destroy()
		RecordingStarted:Destroy()
		FrameChanged:Destroy()

		Replay.VPF:Destroy()

		if Replay.RecordConnection then
			Replay.RecordConnection:Disconnect()
		end
		if Replay.PlayConnection then
			Replay.PlayConnection:Disconnect()
		end

		for _,Object in pairs(Replay.CloneIndex) do
			Object:Destroy()
		end

		Replay = nil
	end

	local function FindFrame(f,t)
		if not f then return end

		t = t or 0

		local FrameDepth = t-(f.Time or 0)

		if (FrameDepth or 0) <= (f.FrameLength or 0) then
			return Replay:GoToFrame(f.ID + (FrameDepth/(f.FrameLength or 0)))
		else
			return FindFrame(Replay.Frames[f.ID+1],t)
		end
	end

	function Replay:Stop()
		if not Replay.Playing then
			warn("Cannot stop playback since playback isn't in progress")
			return
		end

		Replay.Playing = false
		Replay.PlayConnection:Disconnect()
	end

	function Replay:GoToPercent(Percent)
		if not Replay.Recorded then
			warn("Cannot go to percent since there is no recording")
			return
		end

		if DEBUG then
			print("GoToPercent:",Percent)
		end

		return Replay:GoToTime(Replay.RecordingTime*math.clamp(Percent,0,1))
	end

	function Replay:GoToTime(Time)
		if not Replay.Recorded then
			warn("Cannot go to time since there is no recording")
			return
		end

		if DEBUG then
			print("GoToTime:",Time)
		end

		-- Find frame
		for f,t in ipairs(Replay.FrameTimes) do
			if t == Time then
				return Replay:GoToFrame(f)

			else
				local FrameLength = Replay.Frames[f].FrameLength or 0
				if t + FrameLength >= Time then
					return Replay:GoToFrame( (f)+ ((Time-t)/FrameLength) )
				end

			end
		end
	end

	function Replay:GoToFrame(Frame)
		if not Replay.Recorded then
			warn("Cannot go to frame since there is no recording")
			return
		end

		if DEBUG then
			print("GoToFrame:",Frame)
		end

		Frame = math.clamp(Frame,1,Replay.FrameCount)

		local StartFrameData = Replay.Frames[math.floor(Frame)]
		local EndFrameData = Replay.Frames[math.ceil(Frame)]
		local FrameDelta = Frame-math.floor(Frame)
		local Time = StartFrameData.Time+(FrameDelta*(StartFrameData.FrameLength or 0))

		FrameChanged:Fire(
			Frame,
			Time,
			Time/Replay.RecordingTime
		)

		for Object, Data in pairs(StartFrameData.ObjectData) do
			local NextData = EndFrameData.ObjectData[Object]

			if Data.Destroyed then
				Object.Transparency = 1
			else
				if NextData.Destroyed then
					Object.CFrame = serializer.Decode(Data.CFrame)
					Object.Color = serializer.Decode(Data.Color)
					Object.Transparency = Data.Transparency
				else
					Object.CFrame = serializer.Decode(Data.CFrame):lerp(serializer.Decode(NextData.CFrame),FrameDelta)
					Object.Color = serializer.Decode(Data.Color):lerp(serializer.Decode(NextData.Color),FrameDelta)
					Object.Transparency = Data.Transparency + ((NextData.Transparency - Data.Transparency) * FrameDelta)
				end
			end
		end


		return StartFrameData
	end

	function Replay:Play(PlaySpeed,StartTime,Override)
		Replay.RegisteredObjects = {}
		if not Replay.Recorded then
			warn("Cannot play nonexistent recording")
			return
		end
		if Replay.Playing and not Override then
			warn("Cannot play recording since playback is already in progress")
			return
		end

		if Replay.PlayConnection then
			Replay.PlayConnection:Disconnect()
		end

		PlaySpeed = math.clamp(PlaySpeed or 1,0.02,999)

		Replay.Playing = true
		print(Replay.RecordingTime)

		local Timer = math.clamp(StartTime or 0,0,Replay.RecordingTime)
		local Frame = Replay:GoToTime(Timer)

		Replay.PlayConnection = RenderStepped:Connect(function(DeltaTime)
			Timer = Timer+(DeltaTime*PlaySpeed)

			Frame = FindFrame(Frame,Timer)

			if Timer>Replay.RecordingTime or not Frame then
				Replay:Stop()
				script.Parent:WaitForChild("End"):Invoke()
				print("end")
				return
			end
		end)
	end

	function Replay:PlayReplayAsync(framesData)
		if Replay then
			-- Split the framesData into object names and frames data using the "UNIQUESEPERATOR"
			local separatorIndex = "UNIQUESEPERATOR"
			local objectNamesData = framesData:split(separatorIndex)[1]
			local framesDataOnly = framesData:split(separatorIndex)[2]

			-- Deserialize the object names and frames data
			local objectNames = Deserialize(objectNamesData)
			local frames = Deserialize(framesDataOnly)

			-- Clear any previous registration before playing the replay
			Replay:ClearRecording()
			print(objectNames)
			
			-- Register all the objects from the object names list
			for _, name in ipairs(objectNames) do
				print(name)
				local object = game:GetService("Workspace"):FindFirstChild(name, true)
				if object then
					Replay:Register(object)
				end
			end

			Replay.Recorded = true
			Replay.Frames = frames
			Replay.RecordingTime = Replay.Frames[#Replay.Frames].Time
			wait(0.5)
			Replay:Play(1)
		end
	end
	return Replay
end

return Module