-- 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.")