CCCPaste

Roblox remote spy (NEW)

-- LocalScript: Full Remote Tester with Scrollable Console + Multiservice + Resizable UI
-- Place in StarterGui. Development and debugging only.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local Lighting = game:GetService("Lighting")
local SoundService = game:GetService("SoundService")
local StarterGui = game:GetService("StarterGui")
local StarterPack = game:GetService("StarterPack")
local StarterPlayer = game:GetService("StarterPlayer")
local TextChatService = game:GetService("TextChatService")
local UserInputService = game:GetService("UserInputService")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

-- =========================
-- UTILITY FUNCTIONS
-- =========================

-- Trim whitespace
local function trim(s) return s:match("^%s*(.-)%s*$") end

-- Pretty print tables recursively
local function pretty(v, depth)
	depth = depth or 0
	local t = typeof(v)
	if t == "string" then
		return '"' .. v .. '"'
	elseif t == "number" or t == "boolean" or v == nil then
		return tostring(v)
	elseif t == "table" then
		if depth > 4 then return "{...}" end
		local parts = {}
		for k,val in pairs(v) do
			table.insert(parts, string.rep("  ", depth+1) ..
				"["..pretty(k,depth+1).."] = "..pretty(val,depth+1))
		end
		return "{\n"..table.concat(parts,",\n").."\n"..string.rep("  ",depth).."}"
	else
		return "<"..t..">"
	end
end

-- Parse a single token
local function parseToken(tok)
	tok = trim(tok)
	if tok == "" then return nil end
	local s = tok:match('^"(.*)"$') or tok:match("^'(.*)'$")
	if s then return s end
	if tok == "true" then return true end
	if tok == "false" then return false end
	if tok == "nil" then return nil end
	local n = tonumber(tok)
	if n then return n end
	return tok
end

-- Parse CSV arguments into Lua values
local function parseArgs(csv)
	csv = tostring(csv or "")
	if csv == "" then return {} end
	local out, cur, inQuotes, quoteChar = {}, "", false, nil
	for i = 1, #csv do
		local c = csv:sub(i,i)
		if (c == '"' or c == "'") and not inQuotes then
			inQuotes = true
			quoteChar = c
			cur ..= c
		elseif c == quoteChar and inQuotes then
			inQuotes = false
			quoteChar = nil
			cur ..= c
		elseif c == "," and not inQuotes then
			table.insert(out, parseToken(cur))
			cur = ""
		else
			cur ..= c
		end
	end
	if cur ~= "" then table.insert(out, parseToken(cur)) end
	return out
end

-- =========================
-- REMOTE COLLECTION
-- =========================

local function safeGetDescendants(parent)
	local ok, descendants = pcall(function()
		return parent:GetDescendants()
	end)
	return ok and descendants or {}
end

local function collectRemotes()
	local remotes = {}

	local services = {
		ReplicatedStorage, Players, Workspace, StarterGui, StarterPack,
		StarterPlayer, playerGui, Lighting, SoundService, ChatService,
		TextChatService, CoreGui, UserGameSettings, VRService
	}

	for _, svc in ipairs(services) do
		for _, inst in ipairs(safeGetDescendants(svc)) do
			if inst:IsA("RemoteEvent") or inst:IsA("RemoteFunction") then
				table.insert(remotes, inst)
			end
		end
	end

	-- Include Backpack remotes
	if player:FindFirstChild("Backpack") then
		for _, inst in ipairs(safeGetDescendants(player.Backpack)) do
			if inst:IsA("RemoteEvent") or inst:IsA("RemoteFunction") then
				table.insert(remotes, inst)
			end
		end
	end

	-- Include Character tool remotes
	if player.Character then
		for _, inst in ipairs(safeGetDescendants(player.Character)) do
			if inst:IsA("RemoteEvent") or inst:IsA("RemoteFunction") then
				table.insert(remotes, inst)
			end
		end
	end

	-- Include StarterPack items
	for _, inst in ipairs(safeGetDescendants(StarterPack)) do
		if inst:IsA("RemoteEvent") or inst:IsA("RemoteFunction") then
			table.insert(remotes, inst)
		end
	end

	table.sort(remotes, function(a,b) return a:GetFullName() < b:GetFullName() end)
	return remotes
end

-- =========================
-- GUI SETUP
-- =========================

local screenGui = Instance.new("ScreenGui")
screenGui.Name = "RemoteTesterGUI"
screenGui.ResetOnSpawn = false
screenGui.Parent = playerGui
screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

local main = Instance.new("Frame")
main.Size = UDim2.new(0.45,0,0.6,0)
main.Position = UDim2.new(0.5,-260,0.15,0)
main.AnchorPoint = Vector2.new(0.5,0)
main.BackgroundColor3 = Color3.fromRGB(34,34,34)
main.BorderSizePixel = 0
main.Parent = screenGui

-- UIScale for optional zooming
local uiScale = Instance.new("UIScale")
uiScale.Scale = 1
uiScale.Parent = main

-- Titlebar
local titlebar = Instance.new("Frame")
titlebar.Size = UDim2.new(1,0,0,34)
titlebar.BackgroundColor3 = Color3.fromRGB(24,24,24)
titlebar.BorderSizePixel = 0
titlebar.Parent = main

local title = Instance.new("TextLabel")
title.Size = UDim2.new(0.7,0,1,0)
title.Position = UDim2.new(0,8,0,0)
title.BackgroundTransparency = 1
title.Text = "ReplicatedStorage Remote Tester + Scrollable Console"
title.TextColor3 = Color3.fromRGB(230,230,230)
title.Font = Enum.Font.SourceSansSemibold
title.TextSize = 14
title.TextXAlignment = Enum.TextXAlignment.Left
title.Parent = titlebar

local btnClose = Instance.new("TextButton")
btnClose.Size = UDim2.new(0,36,0,22)
btnClose.Position = UDim2.new(1,-44,0,6)
btnClose.Text = "X"
btnClose.Parent = titlebar

local btnMin = Instance.new("TextButton")
btnMin.Size = UDim2.new(0,36,0,22)
btnMin.Position = UDim2.new(1,-88,0,6)
btnMin.Text = "_"
btnMin.Parent = titlebar

-- Content frame
local content = Instance.new("Frame")
content.Size = UDim2.new(1,-16,1,-50)
content.Position = UDim2.new(0,8,0,40)
content.BackgroundTransparency = 1
content.Parent = main

-- Panels
local leftPanel = Instance.new("Frame")
leftPanel.Size = UDim2.new(0.55,0,1,0)
leftPanel.Position = UDim2.new(0,0,0,0)
leftPanel.BackgroundColor3 = Color3.fromRGB(28,28,28)
leftPanel.BorderSizePixel = 0
leftPanel.Parent = content

local rightPanel = Instance.new("Frame")
rightPanel.Size = UDim2.new(0.43,0,1,0)
rightPanel.Position = UDim2.new(0.57,0,0,0)
rightPanel.BackgroundColor3 = Color3.fromRGB(30,30,30)
rightPanel.BorderSizePixel = 0
rightPanel.Parent = content

-- Scrollable console
local consoleFrame = Instance.new("Frame")
consoleFrame.Size = UDim2.new(1,0,0.25,0)
consoleFrame.Position = UDim2.new(0,0,0.75,0)
consoleFrame.BackgroundColor3 = Color3.fromRGB(20,20,20)
consoleFrame.BorderSizePixel = 0
consoleFrame.Parent = main

local consoleScroll = Instance.new("ScrollingFrame")
consoleScroll.Size = UDim2.new(1,-8,1,-8)
consoleScroll.Position = UDim2.new(0,4,0,4)
consoleScroll.BackgroundTransparency = 1
consoleScroll.CanvasSize = UDim2.new(0,0,0,0)
consoleScroll.ScrollBarThickness = 6
consoleScroll.Parent = consoleFrame

local consoleLayout = Instance.new("UIListLayout")
consoleLayout.Padding = UDim.new(0,2)
consoleLayout.Parent = consoleScroll

-- =========================
-- LOGGING FUNCTION
-- =========================

local function logConsole(msg,isError)
	msg = tostring(msg)
	local lines = msg:split("\n")
	for _,line in ipairs(lines) do
		local label = Instance.new("TextLabel")
		label.Size = UDim2.new(1,0,0,16)
		label.BackgroundTransparency = 1
		label.Font = Enum.Font.Code
		label.TextSize = 12
		label.TextXAlignment = Enum.TextXAlignment.Left
		label.Text = os.date("[%H:%M:%S] ") .. line
		label.TextColor3 = isError and Color3.fromRGB(255,80,80) or Color3.fromRGB(0,255,0)
		label.Parent = consoleScroll
	end
	consoleScroll.CanvasSize = UDim2.new(0,0,0,consoleLayout.AbsoluteContentSize.Y + 4)
	consoleScroll.CanvasPosition = Vector2.new(0, consoleScroll.CanvasSize.Y.Offset)
end

-- =========================
-- REMOTES UI
-- =========================

local remotesLabel = Instance.new("TextLabel")
remotesLabel.Size = UDim2.new(1,0,0,26)
remotesLabel.Position = UDim2.new(0,8,0,6)
remotesLabel.BackgroundTransparency = 1
remotesLabel.Text = "Remotes in accessible services"
remotesLabel.TextColor3 = Color3.fromRGB(220,220,220)
remotesLabel.Font = Enum.Font.SourceSansSemibold
remotesLabel.TextSize = 14
remotesLabel.TextXAlignment = Enum.TextXAlignment.Left
remotesLabel.Parent = leftPanel

local refreshBtn = Instance.new("TextButton")
refreshBtn.Size = UDim2.new(0,80,0,24)
refreshBtn.Position = UDim2.new(1,-88,0,6)
refreshBtn.Text = "Refresh"
refreshBtn.Parent = leftPanel

local remotesList = Instance.new("ScrollingFrame")
remotesList.Size = UDim2.new(1,-16,1,-50)
remotesList.Position = UDim2.new(0,8,0,38)
remotesList.CanvasSize = UDim2.new(0,0,0,0)
remotesList.ScrollBarThickness = 6
remotesList.BackgroundTransparency = 1
remotesList.Parent = leftPanel

local uiListLayout = Instance.new("UIListLayout")
uiListLayout.Padding = UDim.new(0,6)
uiListLayout.Parent = remotesList

local selectedLabel = Instance.new("TextLabel")
selectedLabel.Size = UDim2.new(1,-16,0,24)
selectedLabel.Position = UDim2.new(0,8,0,8)
selectedLabel.BackgroundTransparency = 1
selectedLabel.Text = "Selected: (none)"
selectedLabel.TextColor3 = Color3.fromRGB(230,230,230)
selectedLabel.Font = Enum.Font.SourceSans
selectedLabel.TextSize = 14
selectedLabel.TextXAlignment = Enum.TextXAlignment.Left
selectedLabel.Parent = rightPanel

local argsLabel = Instance.new("TextLabel")
argsLabel.Size = UDim2.new(1,-16,0,18)
argsLabel.Position = UDim2.new(0,8,0,36)
argsLabel.BackgroundTransparency = 1
argsLabel.Text = "Arguments (comma separated)"
argsLabel.TextColor3 = Color3.fromRGB(200,200,200)
argsLabel.Font = Enum.Font.SourceSans
argsLabel.TextSize = 12
argsLabel.TextXAlignment = Enum.TextXAlignment.Left
argsLabel.Parent = rightPanel

local argsBox = Instance.new("TextBox")
argsBox.Size = UDim2.new(1,-16,0,36)
argsBox.Position = UDim2.new(0,8,0,56)
argsBox.ClearTextOnFocus = false
argsBox.PlaceholderText = [["hello",5,true]]
argsBox.Text = ""
argsBox.Parent = rightPanel

local fireButton = Instance.new("TextButton")
fireButton.Size = UDim2.new(0,200,0,36)
fireButton.Position = UDim2.new(0,8,0,100)
fireButton.Text = "Fire / Invoke Selected Remote"
fireButton.Parent = rightPanel

-- =========================
-- DRAGGING AND RESIZING
-- =========================

local dragging, dragStartPos, guiStartPos
local resizing, resizeDir, resizeStartPos, resizeStartSize
local edgeThreshold = 8

-- Determine which edge/corner is being hovered
local function getResizeDirection(mousePos)
	local absPos = main.AbsolutePosition
	local absSize = main.AbsoluteSize
	local rightEdge = absPos.X + absSize.X
	local bottomEdge = absPos.Y + absSize.Y
	local nearLeft = math.abs(mousePos.X - absPos.X) <= edgeThreshold
	local nearRight = math.abs(mousePos.X - rightEdge) <= edgeThreshold
	local nearTop = math.abs(mousePos.Y - absPos.Y) <= edgeThreshold
	local nearBottom = math.abs(mousePos.Y - bottomEdge) <= edgeThreshold

	if nearLeft and nearTop then return "TopLeft"
	elseif nearRight and nearTop then return "TopRight"
	elseif nearLeft and nearBottom then return "BottomLeft"
	elseif nearRight and nearBottom then return "BottomRight"
	elseif nearLeft then return "Left"
	elseif nearRight then return "Right"
	elseif nearTop then return "Top"
	elseif nearBottom then return "Bottom"
	end
	return nil
end

-- Dragging
titlebar.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		if resizing then return end
		dragging = true
		dragStartPos = input.Position
		guiStartPos = main.Position
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then
				dragging = false
			end
		end)
	end
end)

UserInputService.InputChanged:Connect(function(input)
	-- Dragging
	if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then
		if resizing then return end
		local delta = input.Position - dragStartPos
		main.Position = UDim2.new(guiStartPos.X.Scale, guiStartPos.X.Offset + delta.X,
			guiStartPos.Y.Scale, guiStartPos.Y.Offset + delta.Y)
	end

	-- Resizing
	if resizing and input.UserInputType == Enum.UserInputType.MouseMovement then
		local delta = input.Position - resizeStartPos
		local newSize = resizeStartSize
		local newPos = main.Position

		if resizeDir == "Right" or resizeDir == "TopRight" or resizeDir == "BottomRight" then
			newSize = UDim2.new(0, math.max(240, resizeStartSize.X.Offset + delta.X), 0, newSize.Y.Offset)
		elseif resizeDir == "Left" or resizeDir == "TopLeft" or resizeDir == "BottomLeft" then
			local newWidth = math.max(240, resizeStartSize.X.Offset - delta.X)
			newSize = UDim2.new(0, newWidth, 0, newSize.Y.Offset)
			newPos = UDim2.new(newPos.X.Scale, newPos.X.Offset + delta.X, newPos.Y.Scale, newPos.Y.Offset)
		end

		if resizeDir == "Bottom" or resizeDir == "BottomLeft" or resizeDir == "BottomRight" then
			newSize = UDim2.new(0, newSize.X.Offset, 0, math.max(180, resizeStartSize.Y.Offset + delta.Y))
		elseif resizeDir == "Top" or resizeDir == "TopLeft" or resizeDir == "TopRight" then
			local newHeight = math.max(180, resizeStartSize.Y.Offset - delta.Y)
			newSize = UDim2.new(0, newSize.X.Offset, 0, newHeight)
			newPos = UDim2.new(newPos.X.Scale, newPos.X.Offset, newPos.Y.Scale, newPos.Y.Offset + delta.Y)
		end

		main.Size = newSize
		main.Position = newPos
	end
end)

main.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		local dir = getResizeDirection(input.Position)
		if dir then
			resizing = true
			resizeDir = dir
			resizeStartPos = input.Position
			resizeStartSize = main.Size
			input.Changed:Connect(function()
				if input.UserInputState == Enum.UserInputState.End then
					resizing = false
					resizeDir = nil
				end
			end)
		end
	end
end)

-- =========================
-- MINIMIZE / CLOSE
-- =========================

btnClose.MouseButton1Click:Connect(function() screenGui:Destroy() end)
local minimized = false
btnMin.MouseButton1Click:Connect(function()
	minimized = not minimized
	content.Visible = not minimized
	consoleFrame.Visible = not minimized
	main.Size = minimized and UDim2.new(0,240,0,34) or UDim2.new(0.45,0,0.6,0)
end)

-- =========================
-- REMOTE LIST POPULATION
-- =========================

local selectedRemote
local function clearList()
	for _,v in ipairs(remotesList:GetChildren()) do
		if v:IsA("TextButton") then v:Destroy() end
	end
end

local function populate()
	clearList()
	local remotes = collectRemotes()
	logConsole("Loading "..#remotes.." remotes...")
	
	spawn(function()  -- run asynchronously to avoid freezing the main thread
		for _,inst in ipairs(remotes) do
			local btn = Instance.new("TextButton")
			btn.Size = UDim2.new(1,-8,0,28)
			btn.BackgroundColor3 = Color3.fromRGB(38,38,38)
			btn.BorderSizePixel = 0
			btn.Text = inst:GetFullName()
			btn.Font = Enum.Font.SourceSans
			btn.TextSize = 13
			btn.TextColor3 = Color3.fromRGB(230,230,230)
			btn.Parent = remotesList

			btn.MouseButton1Click:Connect(function()
				for _,b in ipairs(remotesList:GetChildren()) do
					if b:IsA("TextButton") then
						b.BackgroundColor3 = Color3.fromRGB(38,38,38)
					end
				end
				btn.BackgroundColor3 = Color3.fromRGB(60,60,60)
				selectedRemote = inst
				selectedLabel.Text = "Selected: " .. inst:GetFullName()
			end)

			-- Yield a little to avoid freezing on low-end
			task.wait(0.05)
		end
		remotesList.CanvasSize = UDim2.new(0,0,0,uiListLayout.AbsoluteContentSize.Y + 8)
		logConsole("Finished loading "..#remotes.." remotes.")
	end)
end


refreshBtn.MouseButton1Click:Connect(function()
	populate()
	logConsole("Manual refresh triggered.")
end)

ReplicatedStorage.DescendantAdded:Connect(function(d)
	if d:IsA("RemoteEvent") or d:IsA("RemoteFunction") then populate() end
end)
ReplicatedStorage.DescendantRemoving:Connect(function(d)
	if d:IsA("RemoteEvent") or d:IsA("RemoteFunction") then
		if selectedRemote == d then selectedRemote = nil end
		populate()
	end
end)

-- =========================
-- FIRE REMOTE
-- =========================

fireButton.MouseButton1Click:Connect(function()
	if not selectedRemote then
		logConsole("No remote selected.",true)
		return
	end
	local args = parseArgs(argsBox.Text)
	local ok, res = pcall(function()
		if selectedRemote:IsA("RemoteEvent") then
			selectedRemote:FireServer(table.unpack(args))
			return "Fired RemoteEvent."
		else
			local result = selectedRemote:InvokeServer(table.unpack(args))
			return "InvokeServer returned: " .. pretty(result)
		end
	end)
	if ok then
		logConsole(res)
	else
		logConsole("Error: "..tostring(res),true)
	end
end)

-- =========================
-- INITIALIZATION
-- =========================

populate()
logConsole("Ready.")