Action recorder V1.0 [RECORD ACTIONS]
Key RequiredUniversal

Action recorder V1.0 [RECORD ACTIONS]

Universal ScriptKaneGGKaneGG 796 views 1.1K executesUpdated Sep 17, 2026
5.0/ 5(1 votes)
10100% Positive
Lua Script•1057 lines (34868 chars)
-- Action Recorder V1.3

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera

--// CONFIG
local RED = Color3.fromRGB(255, 40, 40)
local BLACK = Color3.fromRGB(8, 8, 8)
local DARK = Color3.fromRGB(16, 16, 16)
local OUTLINE = 1.5
local FONT = Enum.Font.Code
local TEXT_SIZE = 14

local MOVE_KEYS = {
	W = Vector3.new(0, 0, -1),
	A = Vector3.new(-1, 0, 0),
	S = Vector3.new(0, 0, 1),
	D = Vector3.new(1, 0, 0),
}

local NUMBER_KEYS = {
	One = true, Two = true, Three = true, Four = true, Five = true,
	Six = true, Seven = true, Eight = true, Nine = true, Zero = true
}

--// STATE
local recordings = {}
local nextId = 1
local currentRecordingId = nil
local isRecording = false
local isPlaying = false
local isPlayingSchedule = false
local recordStartTime = 0
local lastActionTime = 0
local rawActions = {}
local schedule = {}
local inScheduler = false
local isMobile = false          -- set by first-time popup
local hasChosenDevice = false

local draggingFrame, dragStart, frameStart = nil, nil, nil

----------------------------------------------------------------
-- HELPERS
----------------------------------------------------------------
local function create(class, props)
	local obj = Instance.new(class)
	for k, v in pairs(props) do obj[k] = v end
	return obj
end

local function addOutline(parent, thickness)
	local stroke = Instance.new("UIStroke")
	stroke.Color = RED
	stroke.Thickness = thickness or OUTLINE
	stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
	stroke.Parent = parent
	return stroke
end

local function makeButton(text, parent, size, pos)
	local btn = create("TextButton", {
		Text = text, Font = FONT, TextSize = TEXT_SIZE, TextColor3 = RED,
		BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = size, Position = pos,
		AutoButtonColor = false, Parent = parent
	})
	addOutline(btn)
	return btn
end

local function makeTopBarDraggable(topBar, targetFrame)
	topBar.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			draggingFrame = targetFrame
			dragStart = input.Position
			frameStart = targetFrame.Position
		end
	end)
end

UserInputService.InputChanged:Connect(function(input)
	if draggingFrame and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
		local delta = input.Position - dragStart
		draggingFrame.Position = UDim2.new(
			frameStart.X.Scale, frameStart.X.Offset + delta.X,
			frameStart.Y.Scale, frameStart.Y.Offset + delta.Y
		)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		draggingFrame = nil
	end
end)

local function getEquippedToolName()
	local char = player.Character
	if not char then return nil end
	for _, t in ipairs(char:GetChildren()) do
		if t:IsA("Tool") then return t.Name end
	end
	return nil
end

local function equipToolByName(toolName)
	if not toolName then return end
	local char = player.Character
	local backpack = player:FindFirstChild("Backpack")
	if not char or not backpack then return end
	local hum = char:FindFirstChildOfClass("Humanoid")
	if not hum then return end

	for _, t in ipairs(char:GetChildren()) do
		if t:IsA("Tool") and t.Name == toolName then return end
	end

	local tool = backpack:FindFirstChild(toolName)
	if tool and tool:IsA("Tool") then
		hum:UnequipTools()
		task.wait()
		hum:EquipTool(tool)
	end
end

----------------------------------------------------------------
-- DEVICE POPUP (first time only)
----------------------------------------------------------------
local function showDevicePopup(onChosen)
	-- Create a temporary ScreenGui for the popup
	local popupGui = create("ScreenGui", {
		Name = "DevicePopup",
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
		DisplayOrder = 100,
		Parent = player:WaitForChild("PlayerGui")
	})

	local popup = create("Frame", {
		BackgroundColor3 = BLACK,
		BorderSizePixel = 0,
		Size = UDim2.new(0, 320, 0, 160),
		Position = UDim2.new(0.5, -160, 0.5, -80),
		Parent = popupGui
	})
	addOutline(popup, 2)

	create("TextLabel", {
		Text = "Are you on Mobile or Computer?",
		Font = FONT,
		TextSize = 16,
		TextColor3 = RED,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 50),
		Parent = popup
	})

	local mobileBtn = makeButton("Mobile", popup, UDim2.new(0, 120, 0, 40), UDim2.new(0.5, -130, 1, -60))
	local computerBtn = makeButton("Computer", popup, UDim2.new(0, 120, 0, 40), UDim2.new(0.5, 10, 1, -60))

	local function choose(isMob)
		isMobile = isMob
		hasChosenDevice = true
		popupGui:Destroy()
		onChosen()
	end

	mobileBtn.MouseButton1Click:Connect(function()
		choose(true)
	end)
	computerBtn.MouseButton1Click:Connect(function()
		choose(false)
	end)
end

----------------------------------------------------------------
-- BUILD UI
----------------------------------------------------------------
local screenGui = create("ScreenGui", {
	Name = "ActionRecorder", ResetOnSpawn = false,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	Parent = player:WaitForChild("PlayerGui")
})

local mainFrame = create("Frame", {
	Name = "Main", BackgroundColor3 = BLACK, BorderSizePixel = 0,
	Size = UDim2.new(0, 560, 0, 400),
	Position = UDim2.new(0.5, -280, 0.5, -200),
	Visible = false, -- hidden until device is chosen
	Parent = screenGui
})
addOutline(mainFrame, 2)

local topBar = create("Frame", {
	BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 34), Parent = mainFrame
})
addOutline(topBar)
makeTopBarDraggable(topBar, mainFrame)

local newBtn = makeButton("  +  NEW", topBar, UDim2.new(0, 90, 1, 0), UDim2.new(0, 0, 0, 0))
newBtn.TextXAlignment = Enum.TextXAlignment.Left

local schedulerBtn = makeButton("Scheduler", topBar, UDim2.new(0, 90, 1, 0), UDim2.new(0, 95, 0, 0))

local title = create("TextLabel", {
	Text = "V1.3  Action Recorder", Font = FONT, TextSize = 15, TextColor3 = RED,
	BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(0, 210, 1, 0),
	Position = UDim2.new(0, 195, 0, 0), Parent = topBar
})
addOutline(title)

local killBtn = makeButton("Kill", topBar, UDim2.new(0, 55, 1, 0), UDim2.new(1, -115, 0, 0))
local minBtn = makeButton("Min", topBar, UDim2.new(0, 50, 1, 0), UDim2.new(1, -50, 0, 0))

local sideBar = create("Frame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0,
	Size = UDim2.new(0, 155, 1, -34), Position = UDim2.new(0, 0, 0, 34), Parent = mainFrame
})
addOutline(sideBar)

local sideScroll = create("ScrollingFrame", {
	BackgroundTransparency = 1, BorderSizePixel = 0,
	Size = UDim2.new(1, -6, 1, -6), Position = UDim2.new(0, 3, 0, 3),
	CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
	Parent = sideBar
})
create("UIListLayout", {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder, Parent = sideScroll})

local mainPage = create("Frame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0,
	Size = UDim2.new(1, -155, 1, -34), Position = UDim2.new(0, 155, 0, 34), Parent = mainFrame
})
addOutline(mainPage)

local emptyLabel = create("TextLabel", {
	Text = "No recording open", Font = FONT, TextSize = 16, TextColor3 = RED,
	BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0), Parent = mainPage
})

local recordingBar = create("Frame", {
	BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 32),
	Visible = false, Parent = mainPage
})
addOutline(recordingBar)

local closeBtn = makeButton("X", recordingBar, UDim2.new(0, 32, 1, 0), UDim2.new(0, 0, 0, 0))

local nameBox = create("TextBox", {
	Text = "Recording 1", Font = FONT, TextSize = TEXT_SIZE, TextColor3 = RED,
	BackgroundColor3 = DARK, BorderSizePixel = 0, ClearTextOnFocus = false,
	Size = UDim2.new(1, -150, 1, 0), Position = UDim2.new(0, 38, 0, 0), Parent = recordingBar
})
addOutline(nameBox)
create("Frame", {BackgroundColor3 = RED, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 1), Position = UDim2.new(0, 0, 1, -1), Parent = nameBox})

local recordBtn = create("TextButton", {
	Text = "●", Font = FONT, TextSize = 22, TextColor3 = RED, BackgroundColor3 = DARK, BorderSizePixel = 0,
	Size = UDim2.new(0, 36, 1, 0), Position = UDim2.new(1, -80, 0, 0), Parent = recordingBar
})
addOutline(recordBtn)

local playBtn = create("TextButton", {
	Text = "▶", Font = FONT, TextSize = 16, TextColor3 = RED, BackgroundColor3 = DARK, BorderSizePixel = 0,
	Size = UDim2.new(0, 36, 1, 0), Position = UDim2.new(1, -38, 0, 0), Parent = recordingBar
})
addOutline(playBtn)

local actionListFrame = create("ScrollingFrame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0,
	Size = UDim2.new(1, -8, 1, -40), Position = UDim2.new(0, 4, 0, 36),
	CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
	Visible = false, Parent = mainPage
})
addOutline(actionListFrame)
local actionLayout = create("UIListLayout", {Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder, Parent = actionListFrame})

----------------------------------------------------------------
-- SCHEDULER UI
----------------------------------------------------------------
local schedulerFrame = create("Frame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 1, 0),
	Visible = false, Parent = mainPage
})

local recBar = create("ScrollingFrame", {
	BackgroundColor3 = DARK, BorderSizePixel = 0,
	Size = UDim2.new(1, -8, 0, 34), Position = UDim2.new(0, 4, 0, 6),
	CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
	ScrollingDirection = Enum.ScrollingDirection.X, Parent = schedulerFrame
})
addOutline(recBar)
create("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder, Parent = recBar})

local addDelayBtn = makeButton("+ Delay", schedulerFrame, UDim2.new(0, 80, 0, 26), UDim2.new(0, 8, 0, 46))
local addRepeatBtn = makeButton("+ Repeat", schedulerFrame, UDim2.new(0, 90, 0, 26), UDim2.new(0, 96, 0, 46))

create("TextLabel", {
	Text = "  Sequence (blocks)", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 20), Position = UDim2.new(0, 0, 0, 78),
	TextXAlignment = Enum.TextXAlignment.Left, Parent = schedulerFrame
})

local seqScroll = create("ScrollingFrame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0,
	Size = UDim2.new(1, -8, 1, -150), Position = UDim2.new(0, 4, 0, 100),
	CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
	Parent = schedulerFrame
})
addOutline(seqScroll)
create("UIListLayout", {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder, Parent = seqScroll})

local playSchedBtn = makeButton("Play Schedule", schedulerFrame, UDim2.new(0, 140, 0, 28), UDim2.new(0, 8, 1, -36))
local clearSchedBtn = makeButton("Clear", schedulerFrame, UDim2.new(0, 70, 0, 28), UDim2.new(0, 156, 1, -36))
local backBtn = makeButton("Back", schedulerFrame, UDim2.new(0, 70, 0, 28), UDim2.new(1, -80, 1, -36))

local minButton = create("TextButton", {
	Text = "AR", Font = FONT, TextSize = 24, TextColor3 = BLACK, BackgroundColor3 = RED, BorderSizePixel = 0,
	Size = UDim2.new(0, 68, 0, 68), Position = UDim2.new(1, -100, 1, -100), Visible = false, Parent = screenGui
})
Instance.new("UICorner", minButton).CornerRadius = UDim.new(1, 0)
local minStroke = Instance.new("UIStroke")
minStroke.Color = BLACK
minStroke.Thickness = 3
minStroke.Parent = minButton

minButton.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		draggingFrame = minButton
		dragStart = input.Position
		frameStart = minButton.Position
	end
end)

local confirmFrame = create("Frame", {
	BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 270, 0, 110),
	Position = UDim2.new(0.5, -135, 0.5, -55), Visible = false, ZIndex = 20, Parent = screenGui
})
addOutline(confirmFrame, 2)
create("TextLabel", {
	Text = "Delete this recording?", Font = FONT, TextSize = 15, TextColor3 = RED, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 45), Parent = confirmFrame
})
local yesBtn = makeButton("Yes", confirmFrame, UDim2.new(0, 90, 0, 32), UDim2.new(0.5, -100, 1, -45))
local noBtn = makeButton("No", confirmFrame, UDim2.new(0, 90, 0, 32), UDim2.new(0.5, 10, 1, -45))

----------------------------------------------------------------
-- APPLY MOBILE / COMPUTER SCALING
----------------------------------------------------------------
local function applyDeviceScaling()
	if isMobile then
		mainFrame.Size = UDim2.new(0.95, 0, 0.7, 0)
		mainFrame.Position = UDim2.new(0.025, 0, 0.15, 0)
		-- slightly larger text / buttons for touch
		TEXT_SIZE = 16
	else
		mainFrame.Size = UDim2.new(0, 560, 0, 400)
		mainFrame.Position = UDim2.new(0.5, -280, 0.5, -200)
	end
	mainFrame.Visible = true
end

----------------------------------------------------------------
-- SIDEBAR + ACTION LIST (copyable)
----------------------------------------------------------------
local function refreshSidebar()
	for _, child in ipairs(sideScroll:GetChildren()) do
		if child:IsA("Frame") then child:Destroy() end
	end
	for i, rec in ipairs(recordings) do
		local item = create("Frame", {
			BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 52),
			LayoutOrder = i, Parent = sideScroll
		})
		addOutline(item)
		create("TextLabel", {
			Text = rec.name, Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
			Size = UDim2.new(1, -6, 0, 20), Position = UDim2.new(0, 4, 0, 3),
			TextXAlignment = Enum.TextXAlignment.Left, Parent = item
		})
		local openBtn = makeButton("Open", item, UDim2.new(0.48, 0, 0, 22), UDim2.new(0.02, 0, 0, 26))
		openBtn.TextSize = 12
		local dupBtn = makeButton("Duplicate", item, UDim2.new(0.48, 0, 0, 22), UDim2.new(0.5, 0, 0, 26))
		dupBtn.TextSize = 11
		openBtn.MouseButton1Click:Connect(function() openRecording(rec.id) end)
		dupBtn.MouseButton1Click:Connect(function() duplicateRecording(rec.id) end)
	end
	sideScroll.CanvasSize = UDim2.new(0, 0, 0, #recordings * 57)
end

local function clearActionList()
	for _, c in ipairs(actionListFrame:GetChildren()) do
		if c:IsA("Frame") or c:IsA("TextBox") or c:IsA("TextLabel") then c:Destroy() end
	end
	actionListFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
end

-- Copyable line (TextBox that is not editable)
local function addSimpleLine(text)
	local box = create("TextBox", {
		Text = "  " .. text,
		Font = FONT,
		TextSize = 13,
		TextColor3 = RED,
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
		ClearTextOnFocus = false,
		TextEditable = false,          -- important: user can still select & copy
		Size = UDim2.new(1, -4, 0, 18),
		TextXAlignment = Enum.TextXAlignment.Left,
		Parent = actionListFrame
	})
end

local function addMouseGroup(group)
	local container = create("Frame", {
		BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, -4, 0, 20), Parent = actionListFrame
	})
	addOutline(container)
	local expanded = false
	local arrow = create("TextButton", {
		Text = "▶", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
		Size = UDim2.new(0, 20, 1, 0), Parent = container
	})
	create("TextLabel", {
		Text = string.format("  Mouse Sequence (%d moves)", #group), Font = FONT, TextSize = 13, TextColor3 = RED,
		BackgroundTransparency = 1, Size = UDim2.new(1, -24, 1, 0), Position = UDim2.new(0, 20, 0, 0),
		TextXAlignment = Enum.TextXAlignment.Left, Parent = container
	})
	local children = {}
	arrow.MouseButton1Click:Connect(function()
		expanded = not expanded
		arrow.Text = expanded and "▼" or "▶"
		for _, child in ipairs(children) do child.Visible = expanded end
		actionListFrame.CanvasSize = UDim2.new(0, 0, 0, actionLayout.AbsoluteContentSize.Y + 8)
	end)
	for _, move in ipairs(group) do
		local box = create("TextBox", {
			Text = "    " .. move.desc,
			Font = FONT, TextSize = 12, TextColor3 = Color3.fromRGB(200, 80, 80),
			BackgroundTransparency = 1, BorderSizePixel = 0,
			ClearTextOnFocus = false, TextEditable = false,
			Size = UDim2.new(1, -4, 0, 16), TextXAlignment = Enum.TextXAlignment.Left,
			Visible = false, Parent = actionListFrame
		})
		table.insert(children, box)
	end
end

local function rebuildActionList(actions)
	clearActionList()
	addSimpleLine("Start")
	local i = 1
	while i <= #actions do
		local act = actions[i]
		if act.data.type == "look" then
			local group = {act}
			local j = i + 1
			while j <= #actions and actions[j].data.type == "look" do
				table.insert(group, actions[j])
				j += 1
			end
			addMouseGroup(group)
			i = j
		else
			addSimpleLine(act.desc)
			i += 1
		end
	end
	addSimpleLine("Stop")
	actionListFrame.CanvasSize = UDim2.new(0, 0, 0, actionLayout.AbsoluteContentSize.Y + 10)
end

----------------------------------------------------------------
-- RECORDING
----------------------------------------------------------------
local function showRecordingUI(rec)
	emptyLabel.Visible = false
	recordingBar.Visible = true
	actionListFrame.Visible = true
	schedulerFrame.Visible = false
	inScheduler = false
	nameBox.Text = rec.name
	rebuildActionList(rec.actions)
end

function openRecording(id)
	currentRecordingId = id
	for _, r in ipairs(recordings) do
		if r.id == id then showRecordingUI(r) break end
	end
end

function duplicateRecording(id)
	local original
	for _, r in ipairs(recordings) do if r.id == id then original = r break end end
	if not original then return end
	local copy = {id = nextId, name = original.name .. " (copy)", actions = table.clone(original.actions)}
	nextId += 1
	table.insert(recordings, copy)
	refreshSidebar()
	refreshSchedulerBar()
end

local function deleteRecording(id)
	for i, r in ipairs(recordings) do
		if r.id == id then table.remove(recordings, i) break end
	end
	if currentRecordingId == id then
		currentRecordingId = nil
		recordingBar.Visible = false
		actionListFrame.Visible = false
		emptyLabel.Visible = true
	end
	refreshSidebar()
	refreshSchedulerBar()
end

local heldKeys = {}
local lastMousePos = Vector2.zero
local lastCamLook = Vector3.zero
local lastSample = 0

local function startRecording()
	if isRecording or isPlaying or isPlayingSchedule or not currentRecordingId then return end
	isRecording = true
	rawActions = {}
	recordStartTime = tick()
	lastActionTime = recordStartTime
	lastSample = recordStartTime
	heldKeys = {}
	lastMousePos = UserInputService:GetMouseLocation()
	if camera then lastCamLook = camera.CFrame.LookVector end
	recordBtn.TextColor3 = Color3.fromRGB(255, 90, 90)
	clearActionList()
	addSimpleLine("Start")
end

local function stopRecording()
	if not isRecording then return end
	isRecording = false
	recordBtn.TextColor3 = RED

	local now = tick()
	if now - lastActionTime > 0.15 then
		table.insert(rawActions, {
			desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
			data = {type = "idle", duration = now - lastActionTime, time = now - recordStartTime}
		})
	end

	local final = {}
	for _, a in ipairs(rawActions) do table.insert(final, a) end

	for _, r in ipairs(recordings) do
		if r.id == currentRecordingId then r.actions = final break end
	end
	openRecording(currentRecordingId)
end

UserInputService.InputBegan:Connect(function(input, gp)
	if gp or not isRecording then return end
	local now = tick()
	local rel = now - recordStartTime

	if now - lastActionTime > 0.12 then
		table.insert(rawActions, {
			desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
			data = {type = "idle", duration = now - lastActionTime, time = rel}
		})
	end

	if input.UserInputType == Enum.UserInputType.Keyboard then
		local key = input.KeyCode.Name
		heldKeys[key] = true
		local data = {type = "keyDown", key = key, time = rel}

		if NUMBER_KEYS[key] then
			task.delay(0.05, function()
				local toolName = getEquippedToolName()
				if toolName then data.toolName = toolName end
			end)
		end

		table.insert(rawActions, {desc = key .. " Down", data = data})
	elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
		local pos = UserInputService:GetMouseLocation()
		table.insert(rawActions, {
			desc = string.format("LMB Click (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
			data = {type = "click", button = "LMB", x = pos.X, y = pos.Y, time = rel}
		})
	elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
		local pos = UserInputService:GetMouseLocation()
		table.insert(rawActions, {
			desc = string.format("RMB Click (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
			data = {type = "click", button = "RMB", x = pos.X, y = pos.Y, time = rel}
		})
	end
	lastActionTime = now
end)

UserInputService.InputEnded:Connect(function(input, gp)
	if gp or not isRecording then return end
	if input.UserInputType == Enum.UserInputType.Keyboard then
		local key = input.KeyCode.Name
		if heldKeys[key] then
			heldKeys[key] = nil
			local now = tick()
			table.insert(rawActions, {
				desc = key .. " Up",
				data = {type = "keyUp", key = key, time = now - recordStartTime}
			})
			lastActionTime = now
		end
	end
end)

UserInputService.InputChanged:Connect(function(input, gp)
	if gp or not isRecording then return end
	if input.UserInputType == Enum.UserInputType.MouseWheel then
		local now = tick()
		local rel = now - recordStartTime
		local delta = input.Position.Z
		table.insert(rawActions, {
			desc = string.format("Scroll %s", delta > 0 and "Up" or "Down"),
			data = {type = "scroll", delta = delta, time = rel}
		})
		lastActionTime = now
	end
end)

RunService.RenderStepped:Connect(function()
	if not isRecording then return end
	local now = tick()
	if now - lastSample < 0.033 then return end
	lastSample = now

	local pos = UserInputService:GetMouseLocation()
	local camLook = camera and camera.CFrame.LookVector or Vector3.zero

	local moved = (pos - lastMousePos).Magnitude > 3
	local looked = (camLook - lastCamLook).Magnitude > 0.02

	if moved or looked then
		local rel = now - recordStartTime
		if now - lastActionTime > 0.08 then
			table.insert(rawActions, {
				desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
				data = {type = "idle", duration = now - lastActionTime, time = rel}
			})
		end
		table.insert(rawActions, {
			desc = string.format("Cursor (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
			data = {
				type = "look",
				x = pos.X, y = pos.Y,
				look = {camLook.X, camLook.Y, camLook.Z},
				time = rel
			}
		})
		lastMousePos = pos
		lastCamLook = camLook
		lastActionTime = now
	end
end)

----------------------------------------------------------------
-- PLAYBACK (camera now follows the character)
----------------------------------------------------------------
local playConnection = nil
local playStartTime = 0
local playIndex = 1
local activeKeys = {}
local currentLook = nil
local originalCameraType = nil
local originalFOV = nil

local function stopPlayback()
	isPlaying = false
	playBtn.Text = "▶"
	if playConnection then
		playConnection:Disconnect()
		playConnection = nil
	end
	activeKeys = {}
	currentLook = nil
	if originalCameraType and camera then
		camera.CameraType = originalCameraType
		originalCameraType = nil
	end
	if originalFOV and camera then
		camera.FieldOfView = originalFOV
		originalFOV = nil
	end
	local char = player.Character
	if char then
		local hum = char:FindFirstChildOfClass("Humanoid")
		if hum then hum:Move(Vector3.zero, true) end
	end
end

local function playRecordingById(id, onFinished)
	local rec
	for _, r in ipairs(recordings) do
		if r.id == id then rec = r break end
	end
	if not rec or #rec.actions == 0 then
		if onFinished then onFinished() end
		return
	end

	isPlaying = true
	playStartTime = tick()
	playIndex = 1
	activeKeys = {}
	currentLook = nil

	local char = player.Character
	local humanoid = char and char:FindFirstChildOfClass("Humanoid")
	local root = char and char:FindFirstChild("HumanoidRootPart")
	if not humanoid or not root then
		if onFinished then onFinished() end
		return
	end

	if camera then
		originalCameraType = camera.CameraType
		originalFOV = camera.FieldOfView
		camera.CameraType = Enum.CameraType.Scriptable
	end

	playConnection = RunService.RenderStepped:Connect(function()
		if not isPlaying then return end
		local elapsed = tick() - playStartTime

		while playIndex <= #rec.actions do
			local act = rec.actions[playIndex]
			local t = act.data.time or 0
			if t > elapsed then break end

			local d = act.data

			if d.type == "keyDown" then
				activeKeys[d.key] = true
				if d.toolName then equipToolByName(d.toolName) end

			elseif d.type == "keyUp" then
				activeKeys[d.key] = nil

			elseif d.type == "look" and d.look then
				currentLook = Vector3.new(d.look[1], d.look[2], d.look[3])

			elseif d.type == "scroll" then
				if camera then
					local fov = camera.FieldOfView
					if d.delta > 0 then
						camera.FieldOfView = math.clamp(fov - 5, 20, 120)
					else
						camera.FieldOfView = math.clamp(fov + 5, 20, 120)
					end
				end
			end
			playIndex += 1
		end

		-- Movement
		local dir = Vector3.zero
		for key, vec in pairs(MOVE_KEYS) do
			if activeKeys[key] then dir += vec end
		end
		if dir.Magnitude > 0 then
			humanoid:Move(dir.Unit, true)
		else
			humanoid:Move(Vector3.zero, true)
		end
		if activeKeys["Space"] then humanoid.Jump = true end

		-- CAMERA FOLLOW FIX
		-- Camera stays with the character while applying recorded look
		if camera and root then
			local head = char:FindFirstChild("Head")
			local camPos = (head and head.Position or root.Position) + Vector3.new(0, 1.5, 0)
			local look = currentLook and currentLook.Magnitude > 0.05 and currentLook or root.CFrame.LookVector
			camera.CFrame = CFrame.lookAt(camPos, camPos + look)
		end

		if playIndex > #rec.actions then
			local lastTime = rec.actions[#rec.actions].data.time or 0
			if elapsed > lastTime + 0.25 then
				stopPlayback()
				if onFinished then onFinished() end
			end
		end
	end)
end

local function playRecording()
	if isPlaying or isRecording or isPlayingSchedule or not currentRecordingId then return end
	playBtn.Text = "■"
	playRecordingById(currentRecordingId, function() playBtn.Text = "▶" end)
end

----------------------------------------------------------------
-- SCHEDULER
----------------------------------------------------------------
function refreshSchedulerBar()
	for _, c in ipairs(recBar:GetChildren()) do
		if c:IsA("Frame") then c:Destroy() end
	end
	local totalWidth = 0
	for i, rec in ipairs(recordings) do
		local chip = create("Frame", {
			BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 120, 0, 26),
			LayoutOrder = i, Parent = recBar
		})
		addOutline(chip)
		create("TextLabel", {
			Text = rec.name, Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
			Size = UDim2.new(1, -28, 1, 0), Position = UDim2.new(0, 4, 0, 0),
			TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, Parent = chip
		})
		local plus = makeButton("+", chip, UDim2.new(0, 24, 0, 22), UDim2.new(1, -26, 0.5, -11))
		plus.TextSize = 16
		plus.MouseButton1Click:Connect(function()
			table.insert(schedule, {type = "recording", id = rec.id, name = rec.name})
			refreshSequenceList()
		end)
		totalWidth += 126
	end
	recBar.CanvasSize = UDim2.new(0, totalWidth + 10, 0, 0)
end

function refreshSequenceList()
	for _, c in ipairs(seqScroll:GetChildren()) do
		if c:IsA("Frame") then c:Destroy() end
	end
	for i, block in ipairs(schedule) do
		local row = create("Frame", {
			BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, -4, 0, 34),
			LayoutOrder = i, Parent = seqScroll
		})
		addOutline(row)

		if block.type == "recording" then
			create("TextLabel", {
				Text = i .. ".  [Recording]  " .. block.name, Font = FONT, TextSize = 13, TextColor3 = RED,
				BackgroundTransparency = 1, Size = UDim2.new(1, -40, 1, 0), Position = UDim2.new(0, 8, 0, 0),
				TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, Parent = row
			})
		elseif block.type == "delay" then
			create("TextLabel", {
				Text = i .. ".  [Delay]", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
				Size = UDim2.new(0, 90, 1, 0), Position = UDim2.new(0, 8, 0, 0), Parent = row
			})
			local box = create("TextBox", {
				Text = tostring(block.seconds or 1), Font = FONT, TextSize = 13, TextColor3 = RED,
				BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 50, 0, 22),
				Position = UDim2.new(0, 100, 0.5, -11), ClearTextOnFocus = false, Parent = row
			})
			addOutline(box)
			create("TextLabel", {
				Text = "seconds", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
				Size = UDim2.new(0, 60, 1, 0), Position = UDim2.new(0, 155, 0, 0), Parent = row
			})
			box.FocusLost:Connect(function()
				local n = tonumber(box.Text)
				if n and n >= 0 then block.seconds = n else box.Text = tostring(block.seconds or 1) end
			end)
		elseif block.type == "repeat" then
			create("TextLabel", {
				Text = i .. ".  [Repeat]", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
				Size = UDim2.new(0, 90, 1, 0), Position = UDim2.new(0, 8, 0, 0), Parent = row
			})
			local box = create("TextBox", {
				Text = tostring(block.times or 2), Font = FONT, TextSize = 13, TextColor3 = RED,
				BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 40, 0, 22),
				Position = UDim2.new(0, 100, 0.5, -11), ClearTextOnFocus = false, Parent = row
			})
			addOutline(box)
			create("TextLabel", {
				Text = "times (next block)", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
				Size = UDim2.new(0, 120, 1, 0), Position = UDim2.new(0, 145, 0, 0), Parent = row
			})
			box.FocusLost:Connect(function()
				local n = tonumber(box.Text)
				if n and n >= 1 then block.times = math.floor(n) else box.Text = tostring(block.times or 2) end
			end)
		end

		local removeBtn = makeButton("X", row, UDim2.new(0, 26, 0, 22), UDim2.new(1, -32, 0.5, -11))
		removeBtn.TextSize = 14
		removeBtn.MouseButton1Click:Connect(function()
			table.remove(schedule, i)
			refreshSequenceList()
		end)
	end
	seqScroll.CanvasSize = UDim2.new(0, 0, 0, #schedule * 39)
end

local function playSchedule()
	if isPlayingSchedule or isPlaying or isRecording or #schedule == 0 then return end
	isPlayingSchedule = true

	task.spawn(function()
		local i = 1
		while i <= #schedule and isPlayingSchedule do
			playSchedBtn.Text = "Playing... (" .. i .. ")"
			local block = schedule[i]

			if block.type == "recording" then
				local finished = false
				playRecordingById(block.id, function() finished = true end)
				while not finished and isPlayingSchedule do task.wait(0.05) end

			elseif block.type == "delay" then
				local waitTime = block.seconds or 1
				local start = tick()
				while tick() - start < waitTime and isPlayingSchedule do task.wait(0.05) end

			elseif block.type == "repeat" then
				local times = block.times or 2
				local nextIdx = i + 1
				if nextIdx <= #schedule then
					local nextBlock = schedule[nextIdx]
					for rep = 1, times do
						if not isPlayingSchedule then break end
						playSchedBtn.Text = "Playing... (" .. nextIdx .. ") x" .. rep
						if nextBlock.type == "recording" then
							local finished = false
							playRecordingById(nextBlock.id, function() finished = true end)
							while not finished and isPlayingSchedule do task.wait(0.05) end
						elseif nextBlock.type == "delay" then
							local waitTime = nextBlock.seconds or 1
							local start = tick()
							while tick() - start < waitTime and isPlayingSchedule do task.wait(0.05) end
						end
					end
					i = nextIdx
				end
			end
			i += 1
		end

		isPlayingSchedule = false
		playSchedBtn.Text = "Play Schedule"
		stopPlayback()
	end)
end

local function showScheduler()
	inScheduler = true
	emptyLabel.Visible = false
	recordingBar.Visible = false
	actionListFrame.Visible = false
	schedulerFrame.Visible = true
	refreshSchedulerBar()
	refreshSequenceList()
end

local function hideScheduler()
	inScheduler = false
	schedulerFrame.Visible = false
	if currentRecordingId then openRecording(currentRecordingId) else emptyLabel.Visible = true end
end

----------------------------------------------------------------
-- BUTTONS
----------------------------------------------------------------
newBtn.MouseButton1Click:Connect(function()
	local rec = {id = nextId, name = "Recording " .. nextId, actions = {}}
	nextId += 1
	table.insert(recordings, rec)
	refreshSidebar()
	refreshSchedulerBar()
end)

schedulerBtn.MouseButton1Click:Connect(function()
	if inScheduler then hideScheduler() else showScheduler() end
end)

killBtn.MouseButton1Click:Connect(function() screenGui:Destroy() end)

minBtn.MouseButton1Click:Connect(function()
	mainFrame.Visible = false
	minButton.Visible = true
end)

minButton.MouseButton1Click:Connect(function()
	mainFrame.Visible = true
	minButton.Visible = false
end)

closeBtn.MouseButton1Click:Connect(function() confirmFrame.Visible = true end)
yesBtn.MouseButton1Click:Connect(function()
	if currentRecordingId then deleteRecording(currentRecordingId) end
	confirmFrame.Visible = false
end)
noBtn.MouseButton1Click:Connect(function() confirmFrame.Visible = false end)

nameBox.FocusLost:Connect(function()
	if currentRecordingId then
		for _, r in ipairs(recordings) do
			if r.id == currentRecordingId then r.name = nameBox.Text break end
		end
		refreshSidebar()
		refreshSchedulerBar()
	end
end)

recordBtn.MouseButton1Click:Connect(function()
	if isRecording then stopRecording() else startRecording() end
end)

playBtn.MouseButton1Click:Connect(function()
	if isPlaying then stopPlayback() else playRecording() end
end)

addDelayBtn.MouseButton1Click:Connect(function()
	table.insert(schedule, {type = "delay", seconds = 1})
	refreshSequenceList()
end)

addRepeatBtn.MouseButton1Click:Connect(function()
	table.insert(schedule, {type = "repeat", times = 2})
	refreshSequenceList()
end)

playSchedBtn.MouseButton1Click:Connect(function()
	if isPlayingSchedule then
		isPlayingSchedule = false
		stopPlayback()
		playSchedBtn.Text = "Play Schedule"
	else
		playSchedule()
	end
end)

clearSchedBtn.MouseButton1Click:Connect(function()
	schedule = {}
	refreshSequenceList()
end)

backBtn.MouseButton1Click:Connect(function() hideScheduler() end)

----------------------------------------------------------------
-- STARTUP
----------------------------------------------------------------
-- First-time device choice
showDevicePopup(function()
	applyDeviceScaling()
	refreshSidebar()
	print("[Action Recorder] V1.3 ready")
end)

Features & Information

Record what you do in game, Then easily replay it. Play recording back to back and with delays using the Scheduler featuer. (IN BETA, Some features may be missing.) Note that roblox blocks some features for security, But controls like camera movement (Although janky) and ordinary keyboard buttons are guaranteed.
#auto farm#recorder

Community Comments (0)

No comments yet for this script.

More Scripts for Universal Scripts