clover's Lua 5.4 scripting engine, with a Roblox-style API for reading the game, drawing overlays and building menus.
An external Lua VM
Scripts run in clover, not inside Roblox. game, workspace and Instance read Roblox memory, so only part of the Roblox API exists. Use pcall when unsure.
Write scripts in the Lua VM editor and run them with Ctrl+Enter. All scripts share one VM; callbacks, tasks and signals keep running about 60 times per second.
Why use it
Real Lua 5.4. Integers and floats, bitwise operators, integer division, goto and the utf8 library, plus familiar helpers such as table.find, math.clamp, string.split and Random.new.
Roblox-style API.game, workspace, Instance, Vector2, Vector3, CFrame, Color3, Enum, task, RunService, UserInputService and TweenService are available with Roblox names.
Executor compatibility. Common executor functions are built in, including readfile, getgenv, loadstring, decompile, getgc, fireserver, queue_on_teleport, crypt and WebSocket.
Fast overlays. Immediate-mode draw.* calls (up to 500000 commands per frame) and retained Drawing.new objects render on top of the game.
Menus in a few lines.Clover:CreateWindow builds windows with tabs, sections, buttons, toggles, sliders, inputs, dropdowns, keybinds and color pickers, and can save and load configs.
Any naming style. Helper namespaces such as draw, utility, mouse and file accept PascalCase, snake_case and camelCase names for the same function.
Safe by default. There is no io, no require and only a small os subset. HTTP, WebSocket, the clipboard, setfflag and the debug library stay off until you allow them, files never leave the workspace folder, the Lua heap is capped at 1 GB, and a runaway loop can always be stopped with Reset VM.
Teleport aware. Scripts queued with queue_on_teleport run in a fresh VM after the game teleports.
Agent ready. A built-in MCP server lets AI coding agents run Lua, read the log, edit editor tabs, manage workspace files and inspect the game.
How the clover VM is built and reset, how it schedules your code, and how it reports output and errors.
One VM for every script
All scripts share one Lua state. Callbacks, connections, tasks and windows keep running until the VM resets, so press Reset VM before re-running a script.
Top-level code blocks the VM
While top-level code runs, nothing else in the VM does. Put long work in task.spawn with task.wait.
There is exactly one VM. clover starts it while it starts its modules, and the first Run starts it if it is not running yet. It is stopped when clover exits. In between it can be reset any number of times.
How the VM is built
A new Lua state is created with an allocator that caps the VM at 1 GB of Lua memory.
The standard libraries are opened and restricted (see Sandbox & Safety). debug is opened only when the unsafe API is allowed.
The clover table is registered, together with global shortcuts such as readfile, getgenv, tick, typeof, notify and WorldToScreen.
The Roblox-style layer is registered: the data types, print and warn, task, bit32, the services and HTTP functions, Drawing, Instance, game and workspace, the script and GC helpers, WebSocket and crypt. Then three Lua chunks add the standard library extensions, Enum with the input and service helpers, and the compatibility shims.
clover.ui is registered, the instruction-count hook is installed, and the Lua prelude builds the draw, utility, keyboard, mouse, cheat and file tables and the Clover menu library.
The scheduler step
A dedicated script thread drives the VM in a loop. Each pass, called a step here, does the following and then sleeps for 16 ms, so the VM steps roughly 60 times per second at most:
Apply a pending reset, if one was requested.
If the master switch is off, clear the draw list and skip the rest of the step.
Run every queued chunk in order: editor runs, MCP lua_execute calls and teleport scripts.
Call every tick callback, then every render callback.
Pump the engine: refresh game and workspace and fire player and child signals, advance tweens, resume task.wait threads whose time is up, start due task.delay and task.defer functions, fire the RunService step signals (Heartbeat, RenderStepped, Stepped) with the step's delta time, deliver key and mouse input events and WebSocket events, then run queued UI callbacks.
Publish the draw commands queued during this step to the overlay.
Reset
A reset throws the whole Lua state away and builds a new one at the start of the next step. It happens when:
you press Reset VM in the Lua VM window, or an agent calls the MCP lua_reset tool;
the Allow unsafe Lua API setting changes, from the settings card or the MCP lua_set_unsafe_api tool;
a teleport is detected while scripts are queued with queue_on_teleport. The queued scripts then run in the new VM (see Teleport).
A reset stops running code (execution interrupted), cancels queued chunks and clears every global, callback, task, connection, tween, Drawing, window and WebSocket. It also empties the teleport queue.
Master switch
Scripts only run while the master switch is on. With it off, Run is refused and running code stops with master switch is off. The VM isn't reset, so callbacks resume when it's turned back on.
Running code
Each run is compiled as a chunk named script and started in a new coroutine, so top-level code can call task.wait.
Because of the chunk name, positions in error messages look like [string "script"]:12:.
Callbacks
Code that must run on every step is registered as a callback instead of being written as a loop. There are two callback lists, documented on the cheat page:
Both lists run on the script thread, in registration order, with no arguments. Render callbacks are the place for draw.* calls, because the draw list is rebuilt every step.
Callbacks are called with a plain protected call on the VM's main thread, so they cannot yield: calling task.wait inside one raises an error. Start a task with task.spawn when a callback needs to wait.
A callback that raises an error is logged as [clover error 3] and removed for good. There is no function to unregister a callback; it stays until it errors or the VM resets.
cheat.register silently ignores event names it does not know.
Tasks and signals
task is a coroutine scheduler. task.wait sleeps (rounded up to whole ticks), task.spawn runs a function now, and task.delay/task.defer run one later. An error ends only that task.
Signals like RunService.Heartbeat and InputBegan fire during each tick. Handlers can't yield, and one that errors is disconnected. :Wait() pauses until the next fire.
Naming conventions
Engine functions live on clover (for example clover.draw_line) and are re-exported in PascalCase, snake_case and camelCase. All spellings are the same function.
Single-word names have two spellings: draw.Line and draw.line, file.Read and file.read.
A few raw functions have a second raw name: clover.printl for clover.print, clover.errorl for clover.error, clover.getbase for clover.module_base, clover.iskeypressed for clover.is_key_down and clover.WorldToScreen for clover.world_to_screen.
Frequently used functions are also globals: readfile, writefile, appendfile, isfile, isfolder, makefolder, delfile, listfiles, getgenv, tick, typeof, base64encode, base64decode, notify, WorldToScreen and world_to_screen. The Globals page lists every global name.
Roblox-style APIs keep Roblox's PascalCase (game:GetService, Instance.new, Vector3.new), and the standard library adds capitalized Roblox-era globals such as Wait, Spawn and Delay.
The menu library is the global Clover and uses PascalCase methods such as Clover:CreateWindow and Window:AddTab (see ui).
Printing and the log
Output goes to one log (newest 4096 lines), shown in Output and Console and readable through the MCP server.
Function
Severity
Source
Arguments joined with
print(...)
info
vm
a tab
warn(...)
warn
vm
a tab
clover.print(...), clover.printl(...)
info
lua
a space
clover.warn(...)
warn
lua
a space
clover.error(...), clover.errorl(...)
error
lua
a space
Arguments are converted with tostring. clover.error only logs; use error to stop code. For toasts use notify (see Globals).
Console prefix
MCP source
Lines from
[lua]
lua
clover.print, clover.warn, clover.error and errors raised by UI element callbacks.
[vm]
vm
The global print and warn, engine messages such as queued, loaded and vm reset complete, [clover error N] reports, and task: and signal: errors.
[rbx]
rbx
Selected output, warning and error lines tailed from the newest Roblox client log in %LOCALAPPDATA%\Roblox\logs. The MCP lua_get_log tool hides them unless asked.
The console does not show the severity. It colors a line red when its text contains error and amber when it contains warn.
Errors
Errors never escape the VM. Where an error is raised decides how it is reported and what survives it:
Raised in
Logged as
Consequence
Compiling a run (syntax error)
[clover error 1]: [string "script"]:LINE: message
The chunk does not run.
Top-level code of a run
[clover error 2]: message
The rest of the chunk is skipped. Anything it already registered stays.
A tick or render callback
[clover error 3]: message
The callback is removed.
A task started by task.spawn, task.delay or task.defer, or any coroutine resumed by the scheduler (including a run's top-level code after its first task.wait)
task: message
That coroutine ends.
A signal connection callback
signal: message
The connection is disconnected.
A UI element callback
ui callback: message (source lua)
The element keeps working.
Code stopped by a reset
Nothing
Interruption errors are not logged.
Console lines carry the [vm] or [lua] prefix in front of these texts. The MCP lua_execute tool also returns the error of its own chunk with a full traceback.
Error codes
Code
Name
Meaning
0
ok
Success. Never logged.
1
compile
A run failed to compile.
2
runtime
A run's top-level code raised an error.
3
callback_failed
A tick or render callback raised an error and was removed.
10
ui_window_not_found
clover.ui.add_tab or clover.ui.window_open_dialog got an unknown window id.
11
ui_tab_not_found
clover.ui.add_section got an unknown tab id.
12
ui_section_not_found
Reserved. Not emitted at the moment.
13
ui_parent_not_found
A clover.ui.add_* element function got an id that is neither a section nor a tab.
14
ui_element_not_found
Reserved. Not emitted at the moment.
15
ui_bootstrap
A built-in Lua chunk (the UI prelude or a standard library chunk) failed to load.
20
mem_bad_type
Reserved. Not emitted at the moment.
30
bind_bad_arg
Reserved. Not emitted at the moment.
31
api_misuse
Reserved. Not emitted at the moment.
Reports are written as [clover error N]: message with severity error and source vm.
Time and memory limits
No execution time limit. A script may run as long as it likes. A loop that never yields freezes the VM (no callbacks, tasks or draws) until you press Reset VM or turn the master switch off.
Always interruptible. An instruction-count hook runs every 1000 VM instructions, and long string pattern matches check periodically, so even while true do end stops on a reset. A C function that is blocked in the middle of a call, such as an HTTP request, finishes first.
Bounded MCP waits.lua_execute waits 100 to 120000 ms (10000 by default) for its chunk. When the wait runs out it reports a timeout, but the script keeps running until it finishes or the VM is reset.
1 GB of Lua memory. The VM's allocator refuses to grow past 1 GB in total, and the allocation then fails with Lua's not enough memory error. The MCP lua_status tool reports the current usage.
The other caps (draw commands, file sizes, listings) are listed under Sandbox & Safety.
Sandbox & Safety
What's removed from standard Lua, what the unsafe API unlocks, and how files are confined.
The unsafe API is off by default
HTTP, WebSocket, clipboard, setfflag and debug need Allow unsafe Lua API in Settings > Lua VM. It's off on every start, and changing it resets the VM.
Files stay in the workspace
Every file function works inside C:\clover\workspace\ only. Allowing the unsafe API does not loosen that.
unsafe-check.lua
local ok, result = pcall(HttpGet, "https://example.com")
if ok then
print("downloaded", #result, "bytes")
else
warn(result)
end
Standard libraries
The VM opens a fixed set of Lua 5.4 libraries and then removes or replaces the parts that could reach outside the sandbox.
Library
Available
Notes
base
Yes
print and warn write to the clover log. load and loadstring accept text only (see below). dofile and loadfile are removed and replaced by workspace-only versions (see Executor Compatibility). require is removed.
The standard library is opened only when the unsafe API is allowed. debug.getupvalue, debug.getupvalues, debug.setupvalue and debug.getinfo always exist, but they are the engine's versions that work on game functions (see Garbage Collector).
bit32
Yes
Provided by the engine, since Lua 5.4 has none (see bit32).
Loading code
load and loadstring are the same function. They only compile Lua source text: precompiled bytecode and the standard reader-function form are rejected.
load(source, [chunkname], [mode], [env])
Compiles source as a Lua chunk and returns it as a function without running it. A leading UTF-8 byte order mark is skipped.
Parameters:
source: string - Lua source text. Passing a function, as standard Lua allows, raises an argument error.
chunkname: string (optional, default "script") - Name used in error messages. As in standard Lua, a name starting with = or @ is shown as written, anything else as [string "..."].
mode: string (optional, default "t") - Must be exactly "t". Any other value, including "b" and "bt", makes the call return nil and Only text Lua chunks are allowed.
env: table (optional) - When this argument is present it becomes the chunk's _ENV. Passing an explicit nil therefore gives the chunk no environment at all.
Returns:
chunk: function? - The compiled chunk, or nil if compiling failed.
message: string? - The syntax error message when chunk is nil.
Aliases:loadstring
lua
local chunk, problem = loadstring("return 6 * 7", "=calc")
if chunk then
print(chunk())
else
warn(problem)
endlocal scoped = load("print(answer)", "=scoped", "t", {print = print, answer = 42})
scoped()
The os subset
os is rebuilt as a new table that holds only four standard functions. os.exit, os.execute, os.getenv, os.remove, os.rename, os.tmpname and os.setlocale do not exist.
Function
Behavior
os.clock()
Standard Lua: elapsed seconds as a float, for measuring intervals.
os.time([t])
Standard Lua: the current time, or the time described by table t.
os.date([format], [time])
Standard Lua: a formatted date string, or a date table with "*t".
os.difftime(t2, t1)
Standard Lua: the difference in seconds.
Allow unsafe Lua API
Enable it in Settings > Lua VM > Allow unsafe Lua API and confirm the dialog. The MCP server can also change it without a dialog.
Every change resets the VM, because the set of opened libraries is fixed when a VM is built. The log then shows Unsafe Lua APIs enabled. Files remain confined to the Clover workspace. or Unsafe Lua APIs disabled; running scripts are being reset.
Feature
Functions
Without the unsafe API
HTTP
request, http_request, HttpGet, game:HttpGet, game:HttpGetAsync, HttpService:GetAsync, HttpService:RequestAsync, HttpService:PostAsync (see http)
Raises setfflag requires Allow unsafe Lua API.setfpscap calls setfflag inside pcall, so it silently does nothing.
Debug library
debug.traceback, debug.sethook, debug.getlocal, debug.getmetatable, debug.setmetatable, debug.getregistry and the rest of the standard debug table
Absent (nil).
Workspace file sandbox
All file access is confined to C:\clover\workspace\. This covers the file table, the global file functions, loadfile, dofile, configs and the MCP workspace_* tools. Rules:
Paths are relative to the workspace. Separate folders with / or \, for example configs/main.cfg.
Absolute and drive paths such as C:\x, C:x, /x or \\server\share are refused, and so are . and .. components.
A path component may not be empty, end with a dot or a space, contain : < > " | ? * or a control character, or be a reserved Windows device name (CON, PRN, AUX, NUL, CONIN$, CONOUT$, CLOCK$, COM0 to COM9, LPT0 to LPT9), with or without an extension.
Paths must be valid UTF-8, contain no NUL byte and be at most 4096 bytes long. An empty path is refused, except that listing an empty path lists the workspace root.
Symbolic links, junctions and other reparse points are refused anywhere along the path, files with more than one hard link are refused, and only regular disk files can be opened. The folders along the path are held open while an operation runs, so they cannot be swapped for a link half-way through.
Reads and writes are limited to 16 MB (16777216 bytes) per file, and appendfile refuses to grow a file past that size.
Writing creates missing parent folders and replaces the whole file.
Deleting works on files only; folders cannot be deleted.
Listing returns at most 10000 names and skips links and names that break the rules above.
The global file functions and the MCP workspace_list tool ignore trailing slashes. The file.* functions and the other MCP workspace tools do not, so file.read("notes/") is refused.
How a refusal is reported depends on the function. readfile, writefile, appendfile, makefolder, delfile and listfiles raise an error, while isfile, isfolder and the file.* functions return false or nil instead. See file for each function.
Limits
Resource
Limit
Lua memory
1 GB for the whole VM. Allocations past it fail with not enough memory.
Execution time
None. Stop runaway code with Reset VM.
Draw commands
500000 per scheduler step. Extra commands are dropped and a warning is logged once until the next reset.
File size
16 MB per read or write.
Directory listing
10000 entries.
Path length
4096 bytes.
HTTP response body
64 MB. Larger responses fail with response too large.
Log
The newest 4096 lines.
Stopping scripts
Scripts can't hang clover: the VM runs on its own thread and a hook checks every 1000 instructions. A reset or the master switch interrupts running code. See Core Concepts.
Standard Library
Which Lua 5.4 libraries are loaded, and the Roblox-style helpers clover adds to table, math and string.
clover runs Lua 5.4.7, not Luau. //, bitwise operators and goto work; continue, += and type annotations don't.
Loaded libraries
Library
Status
Notes
base
Loaded
load and loadstring only accept text chunks; dofile, loadfile and require are removed (see Executor Compatibility for the workspace-based loadfile/dofile shims).
coroutine
Loaded
Full library.
table
Loaded
Plus the extensions below.
string
Loaded
Plus string.split.
math
Loaded
Plus the extensions below.
utf8
Loaded
Full library.
os
Partial
Only os.clock, os.time, os.date and os.difftime.
debug
Unsafe only
Loaded only while the unsafe API is enabled. See Sandbox & Safety.
io, package
Removed
No direct file or module access. Use the file API instead.
A Roblox-style random number generator built on a 64-bit xorshift. Every Random object keeps its own state, so two objects created with the same seed produce the same sequence.
random.lua
local rng = Random.new(1234)
print(rng:NextInteger(1, 6))
print(rng:NextNumber())
print(rng:NextNumber(10, 20))
local deck = rng:Shuffle({1, 2, 3, 4, 5})
print(table.concat(deck, " "))
Random.new([seed])
Creates a generator. Without a seed it mixes os.time, os.clock and an internal counter, so two calls in the same frame still differ. Non-integer seeds are floored.
Parameters:
seed: number (optional) - Seed value.
Returns:
Random
Random:NextNumber([low], [high])
With no arguments, returns a float in [0, 1). With both arguments, returns low + u * (high - low).
Parameters:
low: number (optional)
high: number (optional) - Required when low is given.
Returns:
number
Random:NextInteger(low, high)
Returns an integer from low to high, both inclusive.
Parameters:
low: integer
high: integer
Returns:
integer
Random:NextUnitVector()
Returns a random direction of length 1.
Returns:
Vector3
Random:Shuffle(list)
Shuffles the array in place (Fisher-Yates) and returns the same table.
Parameters:
list: table
Returns:
table - The same table, shuffled.
Random:Clone()
Returns a new generator with the same current state.
Returns:
Random
Setting Up
Open the Lua VM window, run your first script and find the workspace folder your scripts can read and write.
Master switch
Scripts only run while clover's master switch is on. With it off, Run logs master switch is off; script ignored, and code that is already running stops with master switch is off.
Running your first script
Open the Lua VM
In the clover menu, go to Settings and find the Lua VM card. Click Open Lua VM (it reads Focus Lua VM when the window is already open).
Write some code
The window opens with a tab named untitled.lua that already contains a hello-world line. The editor autocompletes globals and API names as you type.
lua
print("hello from clover")
notify("clover is running")
Run it
Click Run or press Ctrl+Enter. The script is queued and runs on the VM thread on its next tick. Its print output and any error show up in the Output panel under the editor and in the Console tab of the side panel.
Keep it running
A script ends when its main chunk returns, but anything it registered keeps running: render callbacks, task timers and signal connections. See Core Concepts.
lua
local frames = 0local white = { R = 1, G = 1, B = 1, A = 1 }
cheat.register("onDraw", function()
frames = frames + 1
draw.Text(20, 20, "frames: " .. frames, white)
end)
The Lua VM window
Control
What it does
Run (Ctrl+Enter)
Queues the active tab's code. Every run shares the same VM, so globals from earlier runs are still there.
Reset VM
Stops every running script, clears queued scripts and drawings, and restarts the VM with a clean state.
Clear editor
Empties the active tab.
Editor settings
Cursor style (Bar, Block, Underscore), blink and blink rate.
Tabs
Each tab is a separate script. New tabs are named untitled-<n>.lua.
Output panel
Everything the VM prints, with Copy and Clear buttons.
Side panel
Explorer, MCP (the MCP Server controls) and Console (live output).
Running the same script twice registers its callbacks twice. Press Reset VM before running a new version of a script that hooks callbacks or signals.
The workspace folder
Scripts can only touch files inside C:\clover\workspace\. clover creates the folder on first use. Every path you pass to file functions and to readfile, writefile, listfiles, loadfile and dofile is relative to it.
save.lua
writefile("notes/hello.txt", "saved at " .. os.date())
print(readfile("notes/hello.txt"))
for _, name in ipairs(listfiles("notes")) do
print(name)
end
Unsafe API
Allow unsafe Lua API in the same card unlocks HTTP, WebSockets and the clipboard. Enabling it resets the VM. Only use it for scripts you trust. See Sandbox & Safety.
Limits
The VM may use up to 1 GB of Lua memory. Allocations past that fail with a Lua memory error.
Scripts are text only. Precompiled bytecode is rejected.
An infinite loop without task.wait blocks the VM. Press Reset VM to stop it.
Input events only fire while Roblox is focused, and only for keys with a VK code in the KeyCode table.
Step by step
Listen for a key press
UserInputService.InputBegan passes an input object and false. Compare input.KeyCode with an Enum.KeyCode item. Mouse buttons arrive with KeyCode.Unknown and a UserInputType of MouseButton1, MouseButton2 or MouseButton3.
lua
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F then
print("F pressed at", input.Position.X, input.Position.Y)
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
print("right click")
endend)
Toggle a feature with a hotkey
Keep the state in a local and flip it on each press. notify shows a toast in the clover menu; its second argument is "success", "warning" or "error".
lua
local enabled = false
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode ~= Enum.KeyCode.Insert thenreturnend
enabled = not enabled
notify(enabled and"Feature on"or"Feature off", enabled and"success"or"warning")
end)
Hold a key
Pair InputBegan with InputEnded, or poll UserInputService:IsKeyDown every tick. IsKeyDown returns false whenever Roblox is not focused.
lua
local holding = false
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.LeftAlt then
holding = trueendend)
UserInputService.InputEnded:Connect(function(input)
if input.KeyCode == Enum.KeyCode.LeftAlt then
holding = falseendend)
RunService.Heartbeat:Connect(function()
if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
print("shift is down")
endend)
Bind an action to several inputs
ContextActionService:BindAction calls your function with the action name, the input state and the input object, for every listed KeyCode or UserInputType. Binding the same name again replaces the old binding. It is not a global; get it with game:GetService.
lua
local ContextActionService = game:GetService("ContextActionService")
ContextActionService:BindAction("Dash", function(name, state, input)
if state == Enum.UserInputState.Begin then
print(name, "via", tostring(input.KeyCode))
endend, false, Enum.KeyCode.Q, Enum.UserInputType.MouseButton3)
Clean up
Connect returns a connection. Call Disconnect when you no longer need it, and UnbindAction for bound actions. A Reset VM removes everything at once.
Press Insert to turn a crosshair on and off, and hold Right mouse to make it bigger.
hotkey-crosshair.lua
local enabled = truelocal aiming = falselocal color = { R = 1, G = 0.8, B = 0.2, A = 1 }
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.Insert then
enabled = not enabled
notify(enabled and"Crosshair on"or"Crosshair off")
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
aiming = trueendend)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton2 then
aiming = falseendend)
cheat.register("onDraw", function()
ifnot enabled thenreturnendlocal w, h = clover.screen_size()
if w == 0thenreturnendlocal size = aiming and14or6
draw.Line(w / 2 - size, h / 2, w / 2 + size, h / 2, color, 2)
draw.Line(w / 2, h / 2 - size, w / 2, h / 2 + size, color, 2)
end)
Things to know
Running a script twice connects its handlers twice. Press Reset VM before re-running.
An error inside a signal handler is logged and that handler is disconnected, so it never runs again. Wrap risky code in pcall if the handler must survive.
InputChanged, TextBoxFocused and the window focus signals exist but never fire.
For keybinds the user can change in a menu, use AddKeybind from the ui library instead.
Build a crosshair settings window with toggles, sliders, a dropdown, a keybind, a color picker and configs.
This guide uses the high-level ui library, which is the global Clover. Read Setting Up first if you have not run a script yet.
Reset before re-running
Windows and render callbacks live until the VM resets. Press Reset VM before running a new version of this script, otherwise you get two windows and two crosshairs.
Step by step
Keep the settings in one table
Element callbacks write into this table and the render callback reads from it, so the two never have to know about each other. Colors are plain { R, G, B } tables from 0 to 1, which is exactly what draw expects.
lua
local settings = {
enabled = true,
visible = true,
size = 8,
thickness = 2,
style = "Cross",
color = { R = 1, G = 0.8, B = 0.2 },
}
Create the window, a tab and a section
Size must be a plain table such as { X = 420, Y = 380 }. A Vector2 is userdata and is ignored, so the window would fall back to 580 by 460.
lua
local Window = Clover:CreateWindow({
Title = "Crosshair",
SubTitle = "clover docs example",
Size = { X = 420, Y = 380 },
})
local Main = Window:AddTab({ Title = "Main" })
local Look = Main:AddSection("Look")
Add a toggle and two sliders
Every element takes one options table. Give it an Id so SaveConfig can store it. Callback runs on the script thread after the user changes the value. Rounding is the number of decimals the slider keeps.
lua
Look:AddToggle({
Id = "CrosshairEnabled",
Title = "Enabled",
Default = settings.enabled,
Callback = function(on)
settings.enabled = on
end,
})
Look:AddSlider({
Id = "CrosshairSize",
Title = "Size",
Min = 2,
Max = 30,
Default = settings.size,
Rounding = 0,
Callback = function(value)
settings.size = value
end,
})
Look:AddSlider({
Id = "CrosshairThickness",
Title = "Thickness",
Min = 1,
Max = 6,
Default = settings.thickness,
Rounding = 1,
Callback = function(value)
settings.thickness = value
end,
})
Add a dropdown and a color picker
A single-choice dropdown passes the chosen option string. The color picker passes a { R, G, B } table. Set the dropdown's Default to its first option: the handle's cached Value always starts at the first option, whatever Default says.
lua
Look:AddDropdown({
Id = "CrosshairStyle",
Title = "Style",
Options = { "Cross", "Dot", "Both" },
Default = "Cross",
Callback = function(choice)
settings.style = choice
end,
})
Look:AddColorpicker({
Id = "CrosshairColor",
Title = "Color",
Default = settings.color,
Callback = function(color)
settings.color = color
end,
})
Add a keybind
In the default Toggle mode the callback receives true or false every time the bound key is pressed. Use your own variable for the state; Keybind:GetState() only reports the latch in Always mode. Key names are A-Z, 0-9, F1-F12, Space, Enter, Insert, LeftShift, RightShift and similar.
lua
local Binds = Main:AddSection("Binds")
Binds:AddKeybind({
Title = "Hide crosshair",
Default = "X",
Mode = "Toggle",
Callback = function(toggled)
settings.visible = not toggled
end,
})
Save and load configs
SaveConfig writes every element with an Id to C:\clover\workspace\configs\<name>.cfg. LoadConfig sets the values back but does not call the callbacks, so copy the loaded values into your settings table yourself.
lua
local Configs = Main:AddSection("Config")
Configs:AddButton({
Title = "Save",
Callback = function()
if Clover:SaveConfig("crosshair") then
notify("Crosshair saved")
endend,
})
Configs:AddButton({
Title = "Load",
Callback = function()
ifnot Clover:LoadConfig("crosshair") then
notify("No saved crosshair yet", "warning")
returnendlocal o = Clover.Options
settings.enabled = o.CrosshairEnabled.Value
settings.size = o.CrosshairSize.Value
settings.thickness = o.CrosshairThickness.Value
settings.style = o.CrosshairStyle.Value
settings.color = o.CrosshairColor.Value
end,
})
Draw from the settings
The render callback runs every pass and only reads the settings table. clover.screen_size returns 0, 0 until the game view is known, so skip drawing until then.
lua
cheat.register("onDraw", function()
ifnot settings.enabled ornot settings.visible thenreturnendlocal w, h = clover.screen_size()
if w == 0thenreturnendlocal cx, cy = w / 2, h / 2local s = settings.size
if settings.style ~= "Dot"then
draw.Line(cx - s, cy, cx + s, cy, settings.color, settings.thickness)
draw.Line(cx, cy - s, cx, cy + s, settings.color, settings.thickness)
endif settings.style ~= "Cross"then
draw.CircleFilled(cx, cy, settings.thickness + 1, settings.color, 16)
endend)
Things to know
Element callbacks run on the script thread during its next pass and cannot yield. Wrap slow work in task.spawn.
SetValue and LoadConfig never fire Callback. Toggles, sliders and inputs move on screen; dropdowns, keybinds and color pickers only update their cached Value.
While a script window exists the overlay takes the mouse and keyboard. Call Window:Destroy() or Clover:Destroy() to give them back to the game.
Errors inside callbacks are caught and logged as ui callback: ... warnings instead of stopping the script.
Let AI agents on your PC run Lua, edit script tabs, browse the game and read memory.
Full access
The token gives full access, including turning on the unsafe API without a prompt. Only share it with agents you trust and stop the server when done.
Starting the server
Open the MCP panel
Open the Lua VM window and pick MCP in the side panel.
Start it
Click Start server and confirm. clover listens on 127.0.0.1 on a random free port and logs MCP server listening on 127.0.0.1:<port> to the console. A new random token is made every time the server starts.
Connect your agent
Use Copy config for clients that read an mcpServers JSON block, or Copy command for Claude Code. Copy URL and Copy token copy the two parts on their own.
Stop it
Click Stop server. Open connections are closed and the token stops working.
Loopback only. Connections from any other address are closed immediately.
Endpoint
POST /mcp with JSON-RPC. Other paths return 404 and other methods return 405.
Auth
Authorization: Bearer <token> is required, otherwise 401.
Browsers
Requests carrying Origin or Sec-Fetch-* headers are rejected with 403, so web pages cannot reach the server. A wrong Host header is also rejected.
Limits
8 connections at once (503 after that), 8 MB request bodies (413), 30 second request deadline (408). Chunked uploads are refused; send Content-Length.
Methods
initialize, ping, tools/list and tools/call.
Tools
Lua VM
Tool
Arguments
What it does
lua_execute
code (required), timeout_ms (100 to 120000, default 10000)
Runs a chunk and returns its return values, everything it printed and any error with a traceback. Scripts that yield keep running in the background after the call returns.
lua_vm_guide
none
Returns a written guide to the VM, its APIs and its limits. The same text is sent as the server instructions on initialize.
lua_get_log
since, limit (1 to 1000, default 100), include_roblox
Reads the VM log. Pass the returned next_since back as since to read only new lines.
lua_clear_log
none
Clears the log.
lua_reset
none
Resets the VM like the Reset VM button and waits up to 10 seconds for it to finish.
lua_status
none
Whether the VM runs, whether the unsafe API is on, Lua memory in use and the log sequence.
lua_set_unsafe_api
enabled (required)
Turns the unsafe API on or off. Resets the VM.
Editor
Tool
Arguments
What it does
editor_list_tabs
none
Lists the tabs open in the Lua VM window. The window has to be rendering for tabs to be visible.
editor_read_tab
name or index (1-based)
Reads a tab's code. Without arguments it reads the active tab.
editor_write_tab
name, code (both required)
Creates a tab or replaces the code of the tab with that name. Tabs hold up to 32767 bytes.
Workspace
Tool
Arguments
What it does
workspace_list
path
Lists a folder in the workspace. Empty lists the root.
workspace_read
path (required)
Reads a text file.
workspace_write
path, content (both required)
Writes a file, creating folders as needed. Up to 16 MB.
workspace_delete
path (required)
Deletes a file.
Game and memory
Tool
Arguments
What it does
game_info
none
PlaceId, GameId, JobId and the local player.
instance_tree
path (dot separated, empty for the DataModel), depth (default 2), max_nodes (default 200)
Prints the Instance tree under a path such as Workspace.Map.
read_game_script
path (required)
Decompiles a LocalScript or ModuleScript by Instance path and returns the source. See Scripts.
memory_status
none
Whether clover is attached to Roblox, with the process id and module base.
memory_read
address (required, decimal or 0x hex string), format, size, count
Reads Roblox process memory. format is hex (default, up to 65536 bytes), cstring, or a typed value (u8i8u16i16u32i32u64i64f32f64pointer) repeated count times.
Remote spy
Tool
Arguments
What it does
remote_spy_status
none
Whether the spy and its hook are active, plus fire and remote counts.
remote_spy_set_enabled
enabled (required)
Turns the remote spy on or off. Turning it on hooks RemoteEvents in the Roblox process and shows the spy window.
remote_spy_list
sort (count, recent or name), filter, limit, include_lua
Lists RemoteEvents the client fired: name, class, path, count and age. Arguments are not captured. include_lua adds a snippet that fires the remote. See Remotes.
remote_spy_clear
none
Forgets every recorded remote.
Globals
Every global name and where it's documented, plus the small global functions.
One shared global table
All scripts share one global table. getgenv() and _G are that table, and values persist until the VM resets.
Where functions live
Most engine functions live on the clover table and are re-exported as draw.*, utility.*, file.* and so on.
clover.error does not throw
clover.error(...) only writes a line to the log. Use Lua's global error(...) to raise an error and stop the current script.
The memory functions exist only on the clover table (clover.memory_read, clover.read_int, ...). See memory.
setclipboard and toclipboard are not globals. Use utility.SetClipboard or clover.setclipboard. See utility.
ContextActionService and StarterGui are only returned by game:GetService. See services.
Functions
print(...)
Converts every argument with tostring rules (so __tostring and __name apply), joins them with a tab and writes one line to the clover log at info level. It replaces Lua's built-in print.
Parameters:
...: any - Values to print. With no arguments an empty line is logged.
The global print tags lines as vm; clover.print tags them as lua.
Same as print but logs at warning severity. It replaces Lua 5.4's warn: control messages such as "@on" and "@off" are not special and are logged like any other text.
Parameters:
...: any - Values to log, joined with a tab.
lua
local camera = workspace and workspace.CurrentCamera
ifnot camera then
warn("no camera yet")
end
notify(text, [status])
Shows a toast notification in the clover overlay, titled "Clover", with text as the message. Returns nothing.
Parameters:
text: string - The message. Numbers are converted to strings; other types raise an error.
status: string (optional, default "success") - "success", "warning" or "error" picks the style. Any other value shows a success notification.
Aliases:clover.notify
Clover:Notify and StarterGui:SetCore("SendNotification", ...) both end up calling this function.
These live only on the clover table. The clover logging functions differ from the globals of the same name: they join arguments with a single space and tag lines as script output (lua).
clover.print(...)
Converts the arguments with tostring rules, joins them with a single space and writes one info line to the clover log.
The UI prelude stores its library table in _G.Clover. It is the entry point for menus; see ui for the full API. Its top-level fields:
Field
Type
Description
Clover.Loaded
boolean
true until Clover:Destroy() runs.
Clover.Unloaded
boolean
false until Clover:Destroy() runs.
Clover.Version
string
"1.0.0".
Clover.Options
table
Element handles stored by their Id option.
Clover:CreateWindow(opts)
method
Creates a window and returns its handle.
Clover:Notify(opts)
method
Calls notify with opts.Content (or opts.Title) and opts.Severity.
Clover:SafeCallback(fn, ...)
method
Calls fn in pcall and logs a failure with clover.warn.
Clover:Destroy()
method
Removes every window and sets Loaded to false.
Clover:SaveConfig(name), Clover:LoadConfig(name)
method
Save and load Clover.Options values in configs/<name>.cfg in the workspace.
Clover:Minimize(), Clover:Maximize()
method
Do nothing; use the window's own methods.
game
Handles to the Roblox DataModel and Workspace, plus GetService and IsLoaded.
game can be nil
game and workspace are nil until the engine finds a DataModel in the attached Roblox process. Check them before indexing, or your script stops with attempt to index a nil value.
Refreshed every tick
game and workspace are reassigned when the DataModel changes (teleport, rejoin). Don't cache them in locals.
Everything here is an Instance
game, workspace and services are Instance values. game.Players works like game:GetService("Players").
game-info.lua
ifnot game then
warn("Roblox is not loaded yet")
returnendlocal Players = game:GetService("Players")
print("place", game.PlaceId, "universe", game.GameId, "server", game.JobId)
print("players online:", #Players:GetPlayers())
local me = Players.LocalPlayer
if me then
print("playing as", me.Name, me.UserId)
end
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined")
end)
Players.PlayerRemoving:Connect(function(player)
print(player.Name .. " left")
end)
Globals
game
The DataModel as an Instance, or nil when no game is loaded. Updated automatically when the game changes.
game:GetFullName() returns an empty string, because full names stop at the DataModel.
Assigning your own value to the global game works until the DataModel address changes, then the engine overwrites it.
workspace
The Workspace as an Instance, or nil. Updated together with game.
There is no capitalised Workspace global; use workspace, game.Workspace or game:GetService("Workspace").
DataModel methods
game:GetService(name)
Returns a service by name, checking three sources in order (see the table below): the engine's Lua shim tables, the global service tables, then the children of the instance it is called on.
Parameters:
name: string - Service name. Numbers are converted to strings; any other type raises an error.
Returns:
Instance | table | nil - The service Instance, one of the Lua service tables, or nil when nothing matches. It never raises an error for an unknown name.
GetService is registered on every Instance. On anything other than game, step 3 searches that instance's own children.
Services are not created on demand. A service that is not a child of the DataModel resolves to nil.
lua
local Players = game:GetService("Players")
local Lighting = game:GetService("Lighting")
local RunService = game:GetService("RunService")
if Lighting then
RunService.Heartbeat:Connect(function()
Lighting.ClockTime = 12end)
end
print(#Players:GetPlayers(), "players")
Order
Name passed
Result
1
ContextActionService, StarterGui (any letter case)
The engine's Lua shim table for that service (see Services).
The global table of the same name. The name must use this exact casing: another casing matches this step but returns nil.
3
Anything else
The first child whose Name or ClassName equals name exactly (case-sensitive), such as Players, Lighting, ReplicatedStorage, Workspace or CoreGui. nil when no child matches.
game:IsLoaded()
Returns true when the engine currently resolves a DataModel in the Roblox process, false otherwise.
Returns:
boolean - Whether a DataModel address is available right now.
It ignores the instance it is called on and does not wait for the place to finish loading. It only reports whether the DataModel pointer resolves.
Registered on every Instance, so workspace:IsLoaded() gives the same answer.
game:HttpGet(url)
Same as the global HttpGet. Returns the response body.
Requires the unsafe API.
Parameters:
url: string - Full http or https URL.
Returns:
string - The response body for a 2xx status.
Aliases:game:HttpGetAsync(url), HttpGet(url)
Raises HTTP access requires Allow unsafe Lua API. when the unsafe API toggle is off (see Sandbox).
Raises HttpGet failed: HTTP <status> for a non-2xx status and HttpGet failed: <reason> when the request itself fails.
The request runs synchronously on the script thread. See http for limits and the other request functions.
DataModel properties
game.PlaceId
The current place id, read as a 64-bit integer from the DataModel.
Read-only.
Returns:
integer - Place id.
game.GameId
The universe (experience) id, read as a 64-bit integer from the DataModel.
Read-only.
Returns:
integer - Universe id.
game.JobId
The server job id string, or an empty string when its pointer is not readable.
Read-only.
Returns:
string - Job id.
game.Workspace
The Workspace Instance, resolved the same way as the workspace global, or nil.
Read-only.
Returns:
Instance | nil - Workspace.
Aliases:game.workspace
game.Loaded
Compatibility stand-in: the game counts as already loaded, so Connect and Once run the callback right away and Wait returns immediately.
This is a plain Lua table, not an engine signal; it has no Fire method.
lua
ifnot game:IsLoaded() then
game.Loaded:Wait()
end
print("ready in place", game.PlaceId)
Players service
game:GetService("Players") returns the real Players Instance. These members are available when the instance's ClassName is exactly Players. For a lighter, pre-read list of players with positions and health, see players.
Players.LocalPlayer
The local Player Instance, or nil when its pointer is not valid (for example while joining).
Read-only.
Returns:
Instance | nil - The local Player.
Players:GetPlayers()
Returns an array of the instance's children whose ClassName is Player, in child order.
Returns:
{Instance} - Array of Player instances. Empty when there are none.
Call it on the Players service. game:GetPlayers() returns an empty table.
lua
for _, player in ipairs(game:GetService("Players"):GetPlayers()) dolocal character = player.Character
print(player.Name, player.DisplayName, character ~= nil)
end
Players.PlayerAdded
Fires with the new Player when someone joins. Players already in the server when the engine starts watching don't fire.
Parameters:
player: Instance - Passed to the callback: the Player that joined.
The connection is not tied to one Players instance: every PlayerAdded connection receives every join.
Returns an engine signal with Connect, Once, Wait and Fire.
Players.PlayerRemoving
Fires with the Player Instance when a previously seen player is no longer a child of the Players service. It is detected after the player is gone, so properties read from it may already be empty.
Parameters:
player: Instance - Passed to the callback: the Player that left.
Like PlayerAdded, every connection receives every leave. Returns an engine signal.
The current Camera Instance, or nil. Available when the instance's ClassName is exactly Workspace.
Read-only.
Returns:
Instance | nil - The Camera. Its FieldOfView is readable and writable (see Instance).
workspace.DistributedGameTime
Seconds of game time since the server started, read as a double.
Read-only.
Returns:
number - Game time in seconds.
clover helpers
clover.getgamename()
Returns the DataModel's Name without going through the game global.
Returns:
string - The DataModel name, or an empty string when there is no DataModel or the name is empty.
Only available on the clover table; there is no global getgamename.
clover.getplaceid()
Returns the current place id without going through the game global.
Returns:
integer - The place id, or 0 when there is no DataModel.
Only available on the clover table; there is no global getplaceid.
Instance
A handle to a live Roblox object: reading a property reads game memory.
Handles, not objects
Every lookup creates a new handle. Handles to the same object are == but not the same table key; key tables by inst.Address. Removed objects aren't tracked.
Only the listed properties exist
Unknown keys look up a child with that name. Writing an unsupported property is silently ignored.
Writes go straight into game memory
Writes only patch this client's memory and the game may overwrite them. Setting Parent and Instance.new block for up to 3 s each.
instance-basics.lua
local player = game:GetService("Players").LocalPlayer
local character = player and player.Character
ifnot character thenreturnendlocal humanoid = character:FindFirstChildOfClass("Humanoid")
local root = character:FindFirstChild("HumanoidRootPart")
if humanoid and root then
print(root:GetFullName(), humanoid.Health, root.Position)
humanoid.WalkSpeed = 24endfor _, child in ipairs(character:GetChildren()) doif child:IsA("BasePart") then
child.Transparency = 0.5endend
character.ChildAdded:Connect(function(child)
print("added", child.Name, child.ClassName)
end)
Constructor
Instance.new(className, [parent])
Creates a real object in Roblox, such as a Part or Folder, and returns it.
Parameters:
className: string - Exact class name, case-sensitive.
parent: Instance (optional, default nil) - Parent to set after creation. Must be an Instance or nil.
Returns:
Instance - The new object.
Failure raises Instance.new failed (<code>); the codes are listed below.
Fails with code 2 on unsupported Roblox builds.
If setting parent fails (code 12) the object has already been created but no handle is returned.
Name cannot be written, so new objects keep their default name.
lua
local ok, part = pcall(Instance.new, "Part", workspace)
if ok then
part.Anchored = true
part.Size = Vector3.new(4, 1, 4)
part.Position = Vector3.new(0, 50, 0)
part.Color = Color3.fromRGB(255, 120, 200)
else
warn(part)
end
Code
Meaning
1
className is an empty string.
2
Roblox is not attached, or the running build is not supported.
3
The class name is not in the reflection registry, or it has no creator.
4, 5, 17
The creator's function table could not be read or does not point at executable code.
6
The creatability check did not run inside the game.
7
The class exists but is not creatable.
8, 9
The create call could not be prepared or did not run.
10, 11
The game returned something that is not a valid object.
12
The object was created but setting its parent failed.
Properties
Instance.Name
The object's name.
Read-only.
Returns:
string - The name, or an empty string when it cannot be read. Names longer than 255 bytes also read as an empty string.
Writing Name is ignored.
Instance.ClassName
The object's class name, read from its class descriptor.
Read-only.
Returns:
string - The class name, or an empty string when it cannot be read.
Instance.Address
The object's memory address. Use it as a table key or with the memory functions.
Read-only.
Returns:
integer - The raw address.
lua
local seen = {}
for _, child in ipairs(workspace:GetChildren()) do
seen[child.Address] = child.Name
end
Instance.Parent
The parent Instance, or nil when the parent pointer is not valid. Writing it reparents the object by calling the game's own SetParent function.
Returns:
Instance | nil - The parent.
Assign an Instance or nil. Any other type raises an error. inst.Parent = nil takes the object out of the tree; there is no Destroy method.
A failed reparent is silent.
The script thread waits for the engine call, up to 3 seconds.
Class-specific properties
Only available when ClassName matches exactly. BasePart covers Part, MeshPart, UnionOperation, TrussPart, WedgePart, CornerWedgePart, SpawnLocation, Seat and VehicleSeat. Parts without physics read zeros and ignore writes.
Class
Property
Type
Access
Notes
BasePart
Position
Vector3
read/write
BasePart
Size
Vector3
read/write
BasePart
CFrame
CFrame
read/write
Writing a Vector3 (or an X/Y/Z table) moves the part and keeps its rotation.
Value on other value classes (CFrameValue, Color3Value, ...) is not supported and falls through to the child lookup.
Value conversion
Floats stored by the game (Transparency, Health, Brightness, ...) read as Lua floats. Ids and integers (UserId, Address, IntValue.Value) read as Lua integers.
Strings are read from game memory and are limited to 255 bytes; longer or unreadable strings read as "".
number writes need a number or a numeric string. Anything else raises a number expected error.
boolean writes use Lua truthiness: nil and false clear the value, anything else sets it.
Vector3 writes accept a Vector3 or a table with X, Y, Z (or x, y, z) fields; missing fields count as 0. Other values are ignored.
Color3 writes accept a Color3 or a table with R, G, B (or r, g, b) fields, each 0 to 1. Other values are ignored.
CFrame writes accept a CFrame (position and rotation), or a Vector3 or table (position only).
After a write the engine handles, it fires that property's GetPropertyChangedSignal connections. For Parent this only happens when the reparent succeeded.
Indexing rules
inst.key is resolved in this order, and the first match wins:
Methods on the Instance metatable (GetChildren, FindFirstChild, ...).
If the stored address is not a plausible pointer, every other key reads nil.
The first direct child whose Name or ClassName equals the key. character.Humanoid therefore also finds a renamed Humanoid when no child is literally named Humanoid before it.
Children with reserved names
A child named like a method or property (a part called Size, a folder called Fire) is shadowed by rule 1, 3 or 4. Use inst:FindFirstChild("Size") to reach it.
Methods
Instance:GetChildren()
Returns the direct children in the game's own child order. Each call builds a new table of new handles.
Returns:
{Instance} - Array of children. Empty when there are none or the address is not valid.
As a safety limit, an object with more than 10,000 children reports none at all.
Instance:GetDescendants()
Returns every descendant, depth first: each child is followed by its own descendants before the next sibling.
Returns:
{Instance} - Array of descendants, capped at 50,000 entries.
Walking a large tree such as game or workspace reads a lot of memory in one call. Prefer FindFirstChild chains or a narrower root when you can.
lua
local count = 0for _, item in ipairs(workspace:GetDescendants()) doif item:IsA("BasePart") then
count = count + 1endend
print(count, "parts in workspace")
Instance:FindFirstChild(name)
Returns the first direct child whose Name or ClassName equals name exactly (case-sensitive), or nil.
Parameters:
name: string - Name (or class name) to look for.
Returns:
Instance | nil - The first match.
Only direct children are searched. A second recursive argument is accepted but ignored; loop over GetDescendants() for a deep search.
Matching ClassName as well as Name differs from Roblox: FindFirstChild("Humanoid") also returns a Humanoid that was renamed.
Instance:FindFirstChildOfClass(className)
Returns the first direct child whose ClassName equals className exactly, or nil. Inheritance is not considered, and names are not matched.
Parameters:
className: string - Exact class name.
Returns:
Instance | nil - The first child of that class.
Instance:FindFirstChildWhichIsA(className)
Returns the first direct child whose Name equals className or whose class passes IsA(className), or nil.
Parameters:
className: string - A class name or one of the class groups.
Returns:
Instance | nil - The first match.
lua
local player = game:GetService("Players").LocalPlayer
local character = player and player.Character
local part = character and character:FindFirstChildWhichIsA("BasePart")
print(part and part.Name)
Instance:WaitForChild(name, [timeout])
Returns the child, waiting (checking every 0.05 s) until it appears or timeout runs out.
Yields.
Parameters:
name: string - Name (or class name) to wait for. The same Name-or-ClassName match as FindFirstChild.
timeout: number (optional, default 5) - Seconds to wait. Negative values count as 0. With 0 a missing child returns nil right away without yielding.
Returns:
Instance | nil - The child, or nil on timeout. A timeout raises no error and logs no warning.
The timeout is counted in 0.05 second steps per check, so the real wait can run slightly longer than timeout.
Can't yield inside engine callbacks; call it from the script body or task.spawn.
lua
local player = game:GetService("Players").LocalPlayer
local gui = player:WaitForChild("PlayerGui", 10)
if gui then
print(gui:GetFullName())
else
warn("PlayerGui did not appear")
end
Instance:IsA(className)
Returns true when the object's ClassName equals className, when className is "Instance", or when className is a class group (table below) that contains the object's ClassName.
Parameters:
className: string - Class name or group name.
Returns:
boolean - Whether the object counts as that class.
There is no full class tree; unlisted abstract names only match an exact ClassName.
IsA class groups
className
Matches these ClassNames
Instance
Every object.
BasePart, PVInstance
Part, MeshPart, BasePart, UnionOperation, TrussPart, WedgePart, CornerWedgePart, SpawnLocation, Seat, VehicleSeat (Model is not a PVInstance here).
Walks up the parent chain (at most 64 levels) and returns true if ancestor is one of the parents. An object is not a descendant of itself.
Parameters:
ancestor: Instance - The possible ancestor. Any non-Instance value returns false instead of raising an error.
Returns:
boolean - Whether ancestor is above this object.
Instance:GetFullName()
Returns the dot-separated path from the topmost ancestor below the DataModel down to this object, for example Workspace.Map.Door. The DataModel itself is never part of the path.
Returns:
string - The path. game:GetFullName() returns an empty string.
At most 64 levels are walked. For an object outside the DataModel the path starts at its topmost ancestor.
Instance:GetPropertyChangedSignal(property)
Returns a signal that fires when a clover script changes property on this object. Changes made by the game aren't seen.
signal - Signal with Connect, Once, Wait and Fire. Callbacks receive no arguments.
Read-only properties such as Name never fire, since writes to them are ignored.
lua
local part = workspace:FindFirstChild("Baseplate")
if part then
part:GetPropertyChangedSignal("Transparency"):Connect(function()
print("Transparency is now", part.Transparency)
end)
part.Transparency = 0.5end
Instance:Fire(...)
Fires a BindableEvent's Event or a ProximityPrompt's Triggered for your own connections only. The game doesn't see it.
Parameters:
...: any (optional) - Values passed to each callback.
Any other class raises Fire: unsupported instance.
To activate a real prompt in the game use fireproximityprompt (see Remotes).
lua
local event = workspace:FindFirstChildOfClass("BindableEvent")
if event then
event.Event:Connect(function(message)
print("got", message)
end)
event:Fire("hello")
end
RemoteEvent:FireServer(...)
Sends a RemoteEvent to the server through the game's own networking code. The object's ClassName must be exactly RemoteEvent. See Remotes for the full rules and error list.
Parameters:
...: any (optional) - Up to 16 values: booleans, numbers, strings, Vector2, Vector3, Color3, CFrame or Instances. nil and tables are rejected.
Aliases:fireserver
Problems raise an error that starts with FireServer: (wrong class, unsupported Roblox build, unsupported argument type, too many arguments).
It does not yield: the script thread waits for the game to run the call, which takes at least 0.6 seconds for a successful send.
lua
local storage = game:GetService("ReplicatedStorage")
local remote = storage and storage:FindFirstChild("SetTeam")
if remote and remote:IsA("RemoteEvent") thenlocal ok, err = pcall(function()
remote:FireServer("Red", 1)
end)
ifnot ok then
warn(err)
endend
RemoteFunction:InvokeServer(...)
Invokes the RemoteFunction and returns the server's reply. See Remotes.
Parameters:
...: any (optional) - Up to 16 arguments, same types as FireServer.
Returns:
... - The values the server returned. Booleans, numbers, strings, Vector2, Vector3, Color3, CFrame and Instances are converted; other types come back as nil.
Aliases:invokeserver
Raises an error after 10 seconds without a reply, or when the server errors.
No other methods exist. inst:Destroy(), inst:Clone() or inst:ClearAllChildren() fall through to the child lookup and fail with an attempt to call error.
Events
Every event is an engine signal with Connect, Once, Wait and Fire. Each access returns a new signal table, but connections are matched by event and object, so connecting through different accesses behaves the same.
Instance.ChildAdded
Fires with each new direct child. Watching starts the first time you read the signal.
Parameters:
child: Instance - Passed to the callback: the new child.
Instance.ChildRemoved
Fires with a direct child that disappeared since the previous tick. The object may already be destroyed, so its properties can read as empty.
Parameters:
child: Instance - Passed to the callback: the removed child.
Instance.DescendantAdded
Fires right after ChildAdded, with the same child. Despite the name it only sees direct children of the object; objects added deeper in the tree do not fire it.
Parameters:
descendant: Instance - Passed to the callback: the new direct child.
Player.CharacterAdded
Fires with the new character model when a player's Character pointer changes to a different model. The engine checks every player once per tick, whether or not anything is connected.
Parameters:
character: Instance - Passed to the callback: the new character model.
Characters that already exist when the engine first scans the players do not fire. There is no CharacterRemoving.
lua
local player = game:GetService("Players").LocalPlayer
player.CharacterAdded:Connect(function(character)
print("respawned as", character:GetFullName())
end)
BindableEvent.Event
Fires only when a clover script calls :Fire(...), not when the game does.
Parameters:
...: any - Passed to the callback: the values given to Fire.
RemoteEvent.OnClientEvent
Exists for compatibility but never fires from the network; only from your own :Fire(...).
Parameters:
...: any - Passed to the callback: the values given to Fire.
ProximityPrompt.Triggered
Fires only when a clover script calls :Fire(...), not when a player uses the prompt.
Parameters:
...: any - Passed to the callback: the values given to Fire.
To trigger the real prompt, or to call the game's own connections, use fireproximityprompt or firesignal (see Remotes).
Metamethods
Operation
Result
tostring(inst), print(inst)
The object's Name, or Instance(nil) when the stored address is not a plausible pointer.
a == b
true when both handles hold the same address. Comparing an Instance with another userdata type (such as a Vector3) raises an error; comparing it with a non-userdata value is false.
Writes a supported property; any other key is ignored.
Attributes
Read game attributes on any Instance and attach your own.
Two layers of values
SetAttribute values live in the script engine and are cleared on VM reset. They override a game attribute with the same name. The game's own copy is only written when that attribute already exists with a compatible type (see Values you can set).
Game attributes depend on the Roblox build
Game attributes are only readable on the Roblox build the engine was verified against. On other builds you only see script values.
Watching attribute changes
There is no GetAttributeChangedSignal. SetAttribute(name, ...) fires GetPropertyChangedSignal(name) instead. Changes made by the game are not observed.
attributes.lua
local player = game:GetService("Players").LocalPlayer
ifnot player thenreturnend
print("Level from the game:", player:GetAttribute("Level"))
player:GetPropertyChangedSignal("Marked"):Connect(function()
print("Marked is now", player:GetAttribute("Marked"))
end)
player:SetAttribute("Marked", true)
for name, value in pairs(player:GetAttributes()) do
print(name, typeof(value), value)
end
Methods
Instance:GetAttribute(name)
Returns the script value stored under name if there is one, otherwise the game's attribute of that name.
Parameters:
name: string - Attribute name, case-sensitive. Numbers are converted to strings; other types raise an error.
Returns:
boolean | number | string | Vector3 | CFrame | nil - The value, or nil when neither layer has it, when the game attribute's type cannot be read, or when game attributes are unavailable on this build.
lua
local part = workspace:FindFirstChild("Door")
if part thenlocal locked = part:GetAttribute("Locked")
print("locked:", locked == true)
end
Instance:SetAttribute(name, value)
Stores a script value on this object, or removes it when value is nil. Fires GetPropertyChangedSignal(name).
Parameters:
name: string - Attribute name. An empty name does nothing (no value stored, no signal).
value: boolean | number | string | Vector3 | CFrame | nil (optional, default nil) - The value. Other types raise SetAttribute: unsupported value type.
Numbers and numeric strings are stored as floats: 5 reads back as 5.0.
Removing a script value with nil does not remove the game's attribute; the game value shows through again.
If the object's address is not a plausible pointer, the call does nothing.
lua
local character = workspace:FindFirstChild("Enemy")
if character then
character:SetAttribute("Target", true)
character:SetAttribute("Seen", os.clock())
character:SetAttribute("Target", nil)
end
Instance:GetAttributes()
Returns every attribute the engine can see on the object as a name = value table. Game attributes are added first, then script values, so a script value replaces a game value with the same name.
Returns:
table - Map from attribute name to value. Empty when there are none. Game attributes whose type cannot be read are left out.
Value types
Values you can set
Lua value
Stored as
Also written into an existing game attribute of type
boolean
boolean
boolean
number, or a numeric string
number (reads back as a float)
a double or float number, when the value is finite (and, for float, within the 32-bit float range). Integer-typed game attributes are not written.
string
string
never
Vector3
Vector3
Vector3, when every component is finite and within ±1,000,000
Writing into the game's copy overwrites the value bytes of the existing entry in place. The engine never adds attributes to the game, removes them, or changes their type.
Game attribute types the engine reads
Game attribute type
Lua value
boolean
boolean
32-bit or 64-bit integer
integer
float or double
number; non-finite values read as nil
string
string, up to 65,536 bytes; longer strings read as nil
Vector3
Vector3; components must be finite and within ±1,000,000, otherwise nil
any other type
nil, and the attribute is left out of GetAttributes
Storage
Script values live in a table inside the engine keyed by object address, so every handle to the same object sees them. They are never saved and are cleared when the VM resets. They are not removed when the game destroys the object, so a new object that reuses the same address can show them until the next reset.
Game attributes are found through the object's component map. The attribute component holds its entries either as a flat array or as a hash table, with at most 4,096 entries.
Every read checks the container's header, collects the entries, then reads the header again. If the game changed it in between (for example by adding an attribute), the result is thrown away and the call behaves as if the object had no game attributes: GetAttribute returns the script value or nil, and GetAttributes returns only script values. String values are verified the same way before they are returned.
Each GetAttribute call scans the entries and compares names in game memory. Read values once and keep them in locals rather than calling it many times per frame.
Services
Lua versions of RunService, UserInputService, TweenService, ContextActionService, StarterGui and HttpService.
Getting a service
RunService, UserInputService, TweenService and HttpService are globals. Get ContextActionService and StarterGui with game:GetService.
Emulated, not the real services
These are engine tables, not Roblox's services. Setting fields like MouseBehavior changes nothing in game.
services-demo.lua
local dot = Drawing.new("Circle")
dot.Radius = 6
dot.Filled = true
dot.Color = Color3.fromRGB(255, 200, 40)
dot.Position = Vector2.new(100, 100)
local info = TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
RunService:BindToRenderStep("follow", Enum.RenderPriority.Last.Value, function(dt)
if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
dot.Position = UserInputService:GetMouseLocation()
endend)
UserInputService.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
TweenService:Create(dot, info, {Radius = 18}):Play()
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
TweenService:Create(dot, info, {Radius = 6}):Play()
endend)
RunService
Heartbeat, RenderStepped and Stepped are one engine event (see Signals). It fires once per scheduler tick with the tick length dt in seconds and is not synchronized with Roblox frames. Handlers cannot yield.
RunService.Heartbeat
Fires once per scheduler tick, after sleeping threads, delays and tweens have been processed.
Parameters:
dt: number - Seconds since the previous tick.
Aliases:RunService.PostSimulation
lua
local frames = 0
RunService.Heartbeat:Connect(function(dt)
frames = frames + 1end)
RunService.RenderStepped
The same event as Heartbeat, under its render name.
Parameters:
dt: number - Seconds since the previous tick.
Aliases:RunService.PreRender
RunService.Stepped
The same event as Heartbeat. Unlike Roblox, which passes (time, dt), handlers receive only dt.
Disconnects the callback bound under name. Unknown names are ignored.
Parameters:
name: string - Name given to BindToRenderStep.
The environment checks always return the same values:
Method
Returns
RunService:IsClient()
true
RunService:IsServer()
false
RunService:IsStudio()
false
RunService:IsRunning()
true
RunService:IsRunMode()
false
RunService:IsEdit()
false
UserInputService
Keyboard and mouse state comes from Windows key polling, not from Roblox. Key and button queries return false or an empty table while Roblox is not the foreground window. Only keys with a mapping in the KeyCode table are reported.
Events
UserInputService.InputBegan
Fires when a key or mouse button goes down while Roblox is focused.
Parameters:
input: InputObject - See InputObject; UserInputState is Begin.
gameProcessed: boolean - Always false.
lua
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if input.KeyCode == Enum.KeyCode.Insert then
print("toggle menu")
endend)
UserInputService.InputEnded
Fires when a key or button whose press was delivered by InputBegan is released, even if Roblox lost focus in between.
Parameters:
input: InputObject - UserInputState is End.
gameProcessed: boolean - Always false.
These Lua-side signals exist so scripts can connect to them, but the engine never fires them (see Lua-side signals):
Signal
Fired by the engine
UserInputService.InputChanged
Never
UserInputService.TextBoxFocused
Never
UserInputService.TextBoxFocusReleased
Never
UserInputService.WindowFocused
Never
UserInputService.WindowFocusReleased
Never
Fields
Field
Value
UserInputService.MouseEnabled
true
UserInputService.KeyboardEnabled
true
UserInputService.TouchEnabled
false
UserInputService.GamepadEnabled
false
UserInputService.VREnabled
false
UserInputService.MouseIconEnabled
true
UserInputService.MouseBehavior
Enum.MouseBehavior.Default
Methods
UserInputService:IsKeyDown(keyCode)
Returns whether the key is held right now.
Parameters:
keyCode: EnumItem - An Enum.KeyCode item.
Returns:
boolean - false when Roblox is not the foreground window or the KeyCode has no key mapping.
lua
if UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then
print("ctrl is held")
end
UserInputService:IsMouseButtonPressed(inputType)
Returns whether a mouse button is held right now.
Parameters:
inputType: EnumItem - Enum.UserInputType.MouseButton1, MouseButton2 or MouseButton3.
Returns:
boolean - false for any other input type or when Roblox is not the foreground window.
UserInputService:GetMouseLocation()
Returns the cursor position in pixels, relative to the top-left corner of the foreground window's client area (normally Roblox).
Returns:
Vector2
UserInputService:GetMouseDelta()
Returns how far the cursor moved, in pixels, since the previous call to GetMouseDelta from any script. The first call returns (0, 0).
Returns:
Vector2
UserInputService:GetKeysPressed()
Returns one InputObject for every mapped keyboard key that is held, in no particular order, each with UserInputStateBegin. Mouse buttons are not included.
Returns:
{InputObject} - Empty when Roblox is not the foreground window.
UserInputService:GetMouseButtonsPressed()
Returns InputObjects for the held mouse buttons, checked in the order MouseButton1, MouseButton2, MouseButton3.
Returns:
{InputObject} - Empty when Roblox is not the foreground window.
UserInputService:GetFocusedTextBox()
Always returns nil.
Returns:
nil
UserInputService:GetLastInputType()
Always returns Enum.UserInputType.Keyboard.
Returns:
EnumItem
UserInputService:IsGamepadButtonDown()
Always returns false; arguments are ignored.
Returns:
boolean
InputObject
Input events, GetKeysPressed and GetMouseButtonsPressed produce plain Lua tables with the fields below. A new table is built for every event.
Field
Type
Value
KeyCode
EnumItem
The key's Enum.KeyCode item; Enum.KeyCode.Unknown for mouse buttons
UserInputType
EnumItem
MouseButton1 (left), MouseButton2 (right), MouseButton3 (middle) or Keyboard
UserInputState
EnumItem
Begin for a press, End for a release
Position
Vector3
Cursor position in pixels as (x, y, 0), read when the object is built
Delta
Vector3
Always (0, 0, 0)
TweenService
Animates number, Vector2, Vector3 and Color3 values on a Lua table or a Drawing object. Tweens advance once per scheduler tick and write each new value straight into the target.
Local targets only
TweenService:Create only tweens Lua tables and Drawing objects, not game Instances. Every goal property must already exist with the same type.
TweenService:Create(target, tweenInfo, goals)
Creates a stopped tween that moves each property in goals from its value at Play time to the goal value.
Parameters:
target: table | Drawing - Object whose properties are animated.
tweenInfo: TweenInfo | table | number (optional) - Timing. A number is used as Time, nil uses every default, and any table with TweenInfo fields works.
goals: table - Property name to end value. Values must be finite numbers, Vector2, Vector3 or Color3.
Validates the timing values and returns them as a plain table. Call it with a dot.
Parameters:
time: number (optional, default 1) - Seconds for one pass; finite and at least 0.
easingStyle: EnumItem | string (optional, default "Quad") - An Enum.EasingStyle item or its name.
easingDirection: EnumItem | string (optional, default "Out") - An Enum.EasingDirection item or its name.
repeatCount: number (optional, default 0) - Extra cycles after the first. A whole number from -1 to 1000000; -1 repeats forever and needs a non-zero time or delayTime.
reverses: boolean (optional, default false) - Play each cycle forward, then backward to the start value.
delayTime: number (optional, default 0) - Seconds to wait at the start of every cycle; finite and at least 0.
Returns:
info: table - Fields Time, EasingStyle, EasingDirection, RepeatCount, Reverses and DelayTime. Style and direction are stored as name strings such as "Quad", not as EnumItems.
Invalid values raise an error immediately.
lua
local info = TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, true, 0.1)
print(info.Time, info.EasingStyle, info.Reverses)
One cycle is DelayTime + Time (doubled Time with Reverses), repeated RepeatCount + 1 times. The final value is written before Completed fires.
Styles: Linear, Sine, Back, Quad, Quart, Quint, Bounce, Elastic, Exponential, Circular, Cubic (see EasingStyle). Directions: In, Out, InOut.
Tween
Tween properties are read-only: assigning any key raises Tween properties are read-only. After Destroy, Play, Pause and Cancel raise Tween has been destroyed.
Tween:Play()
Starts or resumes the tween. Cancels other tweens on the same target that animate the same properties.
lua
local box = {Size = Vector2.new(10, 10)}
local grow = TweenService:Create(box, 0.25, {Size = Vector2.new(40, 40)})
grow:Play()
Tween:Pause()
Stops a playing tween and keeps its progress; PlaybackState becomes Paused. Completed does not fire.
Tween:Cancel()
Stops the tween and fires Completed with Cancelled. Values already written stay.
Tween:Destroy()
Stops the tween without firing Completed, disconnects every Completed handler and releases the target. Calling it again does nothing.
Tween.PlaybackState
Current state: Begin after creation, then Delayed, Playing, Paused, Completed or Cancelled. These are bare Enum.PlaybackState items, so compare them with ==.
Read-only.
Returns:
EnumItem
Tween.Completed
Fires when the tween finishes or is cancelled. Handlers may yield.
Read-only.
Parameters:
playbackState: EnumItem - Enum.PlaybackState.Completed or Enum.PlaybackState.Cancelled.
lua
local state = {Value = 0}
local tween = TweenService:Create(state, 1, {Value = 100})
tween.Completed:Connect(function(playbackState)
print("tween ended:", playbackState.Name, state.Value)
end)
tween:Play()
Tween.Instance
The target table or Drawing object; nil after Destroy.
Read-only.
Returns:
table | Drawing | nil
Tween.TweenInfo
A fresh copy of the tween's TweenInfo table each time it is read.
Read-only.
Returns:
table
Tween.Error
The error that stopped the tween during an update, otherwise nil.
Read-only.
Returns:
string | nil
ContextActionService
Get it with game:GetService("ContextActionService"). Bindings are built on UserInputService.InputBegan and InputEnded, so the same key mapping and focus rules apply, and the callback runs on both press and release.
Binds callback to the given inputs under name, replacing any binding with the same name.
Parameters:
name: string - Action name, passed back to the callback.
callback: function - Called as callback(name, state, input) with an InputObject.
createTouchButton: boolean - Ignored.
...: EnumItem - Enum.KeyCode or Enum.UserInputType items. An input matches when its KeyCode or its UserInputType equals one of them.
The callback runs inside a signal handler, so it cannot yield.
If the callback errors, its connection is dropped and a later UnbindAction can remove an unrelated one.
lua
local CAS = game:GetService("ContextActionService")
CAS:BindAction("sprint", function(name, state, input)
if state == Enum.UserInputState.Begin then
print(name, "started with", input.KeyCode)
else
print(name, "stopped")
endend, false, Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift)
Removes the binding with that name. Unknown names are ignored.
Parameters:
name: string - Name given to BindAction.
ContextActionService:UnbindAllActions()
Removes every binding.
ContextActionService:GetBoundActionInfo()
Always returns an empty table.
Returns:
table
StarterGui
Get it with game:GetService("StarterGui"). Only notifications do anything.
StarterGui:SetCore(name, value)
Only "SendNotification" does anything: it shows a notify toast with Title: Text.
Parameters:
name: string - Only "SendNotification" is handled.
value: table - Title and Text, converted with tostring. Duration does not control how long the notification stays, and other keys are ignored.
lua
local StarterGui = game:GetService("StarterGui")
StarterGui:SetCore("SendNotification", {Title = "clover", Text = "Script loaded"})
StarterGui:GetCore()
Always returns nil.
Returns:
nil
StarterGui:SetCoreGuiEnabled()
Does nothing.
StarterGui:GetCoreGuiEnabled()
Always returns true.
Returns:
boolean
HttpService
The global HttpService wraps the engine's HTTP and JSON functions. Network calls need the unsafe API toggle; see http for request options, limits and the response format.
Member
Description
HttpService:GetAsync(url)
GET request. Returns the body, raises an error on failure. Needs the unsafe API
HttpService:PostAsync(url, data, [contentType])
POST with Content-Type defaulting to application/json. Returns the body, raises HTTP <status> when the request does not succeed. Needs the unsafe API
HttpService:RequestAsync(options)
Same as request(options): returns a response table. Needs the unsafe API
HttpService:JSONEncode(value)
Encodes a Lua value as a JSON string
HttpService:JSONDecode(json)
Decodes a JSON string; raises JSONDecode: invalid JSON on bad input
HttpService:GenerateGUID([wrapInBraces])
Random version 4 style GUID in lowercase hex, wrapped in braces unless the argument is false
HttpService:UrlEncode(text)
Percent-encodes every character except letters, digits, -, _, . and ~
HttpService.HttpEnabled
Always true
players
Plain-table snapshots of the players in the server.
Snapshots, not Instances
Each call returns fresh tables from the engine's player scanner (refreshed about every 80 ms). They don't update; call again. For Instances use game:GetService("Players").
Cheap enough for every frame
Both functions are safe to call from a render callback. Each call costs one memory read per player for pos, so call clover.players() once per frame and reuse the result.
list-players.lua
local me = clover.local_player()
ifnot me then
print("no local player yet")
returnendfor _, p in ipairs(clover.players()) doifnot p.is_local and p.humanoid ~= 0thenlocal dx = p.pos.x - me.pos.x
local dy = p.pos.y - me.pos.y
local dz = p.pos.z - me.pos.z
local distance = math.sqrt(dx * dx + dy * dy + dz * dz)
print(string.format("%s (%d) %.0f studs", p.name, p.user_id, distance))
endend
Functions
clover.players()
Returns every player the scanner currently knows about, the local player included.
Returns:
players: table - Array of player tables, indexed from 1. Never nil: it is an empty table outside a game, while the game is loading, or before the scanner has found the Players service.
The order of the array is not stable between calls. Sort it yourself if you need a fixed order.
Entries with an empty name and a user_id of 0 are left out.
Players without a character are normally still listed, with humanoid set to 0 and pos at 0, 0, 0. Check for that before projecting pos.
clover.local_player()
Returns the entry for your own player from the same snapshot that clover.players() uses.
Returns:
player: table | nil - A player tablewithout the is_local field, or nil when the snapshot has no local player (outside a game, while loading, or before the first scan).
lua
local me = clover.local_player()
if me then
print(me.name, me.user_id, me.pos.x, me.pos.y, me.pos.z)
end
Player table
Field
Type
Description
name
string
The player's display name, or the account name when the display name is empty.
user_id
integer
The player's UserId.
team
integer
A team identifier. Normally the memory address of the player's Team instance, and 0 when they have no team. In a few games with built-in support the scanner substitutes its own small ids (for example 1 and 2), and free-for-all modes can report 0 for everyone. Only compare it with other team values; it is not a name.
is_local
boolean
true for your own player. Present only in clover.players() entries.
humanoid
integer
Memory address of the character's Humanoid, or 0 when the player has no character or no Humanoid was found.
address
integer
Memory address of the Player instance, usable with the memory functions. It can be 0 in games where the scanner builds players from character models instead of Player instances.
pos
table
World position of the character's HumanoidRootPart as { x, y, z } (lowercase keys, plain numbers, not a Vector3). Read when the function is called. All three are 0 when no HumanoidRootPart is known, for example while the player has no character.
To use pos with the data types, convert it: Vector3.new(p.pos.x, p.pos.y, p.pos.z). To put it on screen, pass the three numbers to world_to_screen.
enemies.lua
localfunction enemies()
local me = clover.local_player()
local out = {}
for _, p in ipairs(clover.players()) dolocal teammate = me ~= niland p.team ~= 0and p.team == me.team
ifnot p.is_local andnot teammate and p.humanoid ~= 0then
out[#out + 1] = p
endendreturn out
end
print(#enemies(), "enemies")
draw
Immediate-mode 2D drawing that you repeat every frame, plus world-to-screen projection.
Redraw every frame
Each pass's draw commands replace the previous ones, so draw from a callback that runs every pass: clover.on_render(fn) or cheat.register("onDraw", fn) (see cheat).
Pixels, top-left origin
All values are screen pixels with (0, 0) at the top-left. Use world_to_screen for world positions.
Colors are 0-1, and Color3 is not accepted
Colors are a { R, G, B, A } table or four numbers, each 0 to 1. Color3 raises an error. See Colors.
Limits and layering
Up to 500000 commands per pass. They render in call order, behind Drawing objects and the clover menu.
crosshair.lua
local white = { R = 1, G = 1, B = 1, A = 1 }
local shade = { R = 0, G = 0, B = 0, A = 0.5 }
clover.on_render(function()
local w, h = clover.screen_size()
if w == 0thenreturnendlocal cx, cy = w / 2, h / 2
draw.Line(cx - 8, cy, cx + 8, cy, white, 1)
draw.Line(cx, cy - 8, cx, cy + 8, white, 1)
local label = "clover"local tw, th = draw.GetTextSize(label)
draw.RectangleFilled(10, 10, 10 + tw + 12, 10 + th + 8, shade, 4)
draw.Text(16, 14, label, white)
end)
Each function also has snake_case and camelCase aliases plus the raw clover.draw_* name. Only draw.GetTextSize returns a value.
Colors
Every [color] parameter on this page is read the same way:
Form
Slots used
Details
Table
1
Fields R, G, B, A (or lowercase r, g, b, a). A missing or non-number field counts as 1. Array-style tables such as {1, 0, 0} are not read, so they come out white.
Numbers
4
Four separate numbers r, g, b, a. Each missing or nil number counts as 1.
Omitted
0
Opaque white.
All components are clamped to 0-1. Use n / 255 to convert byte values.
The number form takes four argument slots, so the parameters after it shift by four. Always pass all four numbers: draw.Line(0, 0, 50, 50, 1, 0, 0, 2) reads 2 as alpha (clamped to 1) and leaves the thickness at its default.
You cannot skip the color and still pass a later argument. A nil in the color slot is read as the red number, and the next arguments are consumed as green, blue and alpha.
Color3 values are userdata and are rejected with an argument error. Convert them with { R = c.R, G = c.G, B = c.B, A = 1 }. The value of a Library colorpicker is already a plain { R, G, B } table and can be passed directly.
colors.lua
local red = { R = 1, G = 0, B = 0, A = 1 }
local accent = Color3.fromRGB(255, 170, 0)
localfunction rgba(c, a)
return { R = c.R, G = c.G, B = c.B, A = a or1 }
end
clover.on_render(function()
draw.Line(10, 10, 200, 10, red, 2)
draw.Line(10, 20, 200, 20, 0, 1, 0, 1, 2)
draw.Line(10, 30, 200, 30, rgba(accent, 0.5), 2)
end)
Shapes
draw.Line(x1, y1, x2, y2, [color], [thickness])
Draws a straight line from (x1, y1) to (x2, y2).
Parameters:
x1: number - Start x in pixels.
y1: number - Start y in pixels.
x2: number - End x in pixels.
y2: number - End y in pixels.
color: table | numbers (optional, default white) - See Colors.
thickness: number (optional, default 1) - Line width in pixels. Zero or negative values draw at 1.
Draws the outline of an axis-aligned rectangle. (x1, y1) and (x2, y2) are two opposite corners, not a position and a size: pass x + w, y + h as the second pair.
Parameters:
x1: number - Left edge in pixels.
y1: number - Top edge in pixels.
x2: number - Right edge in pixels.
y2: number - Bottom edge in pixels.
color: table | numbers (optional, default white) - See Colors.
thickness: number (optional, default 1) - Outline width in pixels. Zero or negative values draw at 1.
rounding: number (optional, default 0) - Corner radius in pixels, applied to all four corners.
Aliases:draw.rectangle, clover.draw_rect
lua
local box = { R = 0.3, G = 0.8, B = 1, A = 1 }
local x, y, w, h = 100, 100, 80, 40
clover.on_render(function()
draw.Rectangle(x, y, x + w, y + h, box, 2, 6)
end)
Draws the outline of a circle centered on (x, y). The number of segments is chosen automatically from the radius.
Parameters:
x: number - Center x in pixels.
y: number - Center y in pixels.
radius: number - Radius in pixels.
color: table | numbers (optional, default white) - See Colors.
thickness: number (optional, default 1) - Outline width in pixels. Zero or negative values draw at 1.
Aliases:draw.circle, clover.draw_circle
draw.CircleFilled(x, y, radius, [color], [segments])
Fills a circle centered on (x, y).
Parameters:
x: number - Center x in pixels.
y: number - Center y in pixels.
radius: number - Radius in pixels.
color: table | numbers (optional, default white) - See Colors.
segments: integer (optional, default 0) - Number of segments. 0 or less picks a count automatically from the radius; a small value such as 6 draws a polygon.
Fills the rectangle between two opposite corners with a two-color linear gradient. By default color1 is on the left edge and color2 on the right edge. There is no rounding.
color2: table | numbers (optional, default white) - End color.
vertical: boolean (optional, default false) - true for top-to-bottom. Defaults to left-to-right.
Aliases:draw.gradient, clover.draw_gradient
lua
local red = { R = 1, G = 0, B = 0, A = 1 }
local blue = { R = 0, G = 0, B = 1, A = 1 }
local clear = { R = 0, G = 0, B = 0, A = 0 }
local dark = { R = 0, G = 0, B = 0, A = 0.8 }
clover.on_render(function()
draw.Gradient(20, 60, 220, 80, red, blue)
draw.Gradient(20, 90, 220, 140, clear, dark, true)
end)
Draws connected line segments through a list of points.
Parameters:
points: table - Array of { X, Y } tables or flat numbers read as x, y pairs. Vector2 values are skipped. Needs at least two points.
color: table | numbers (optional, default white) - See Colors.
thickness: number (optional, default 1) - Line width in pixels. Zero or negative values draw at 1.
closed: boolean (optional, default false) - Any truthy value also connects the last point back to the first. You must pass thickness to reach this argument.
Aliases:draw.polyline, clover.draw_polyline
lua
local cyan = { R = 0.4, G = 0.8, B = 1, A = 1 }
local roof = { { x = 100, y = 100 }, { x = 160, y = 60 }, { x = 220, y = 100 } }
local zigzag = { 100, 140, 130, 120, 160, 140, 190, 120 }
clover.on_render(function()
draw.Polyline(roof, cyan, 2, true)
draw.Polyline(zigzag, cyan, 1)
end)
Text
Text uses the overlay's ImGui font at its default size. There is no font or size argument; for sized text use a Text object from Drawing.
draw.Text(x, y, text, [color])
Draws a string with its top-left corner at (x, y). Newlines start a new line.
Parameters:
x: number - Left edge in pixels.
y: number - Top edge in pixels.
text: string - The text. Numbers are converted to strings; other types raise an error.
color: table | numbers (optional, default white) - See Colors.
Aliases:draw.text, clover.draw_text
draw.TextOutlined(x, y, text, [color], [outlineColor])
Draws text with a 1-pixel outline, readable on any background. Good for ESP labels.
Parameters:
x: number - Left edge in pixels.
y: number - Top edge in pixels.
text: string - The text. Numbers are converted to strings; other types raise an error.
color: table | numbers (optional, default white) - Fill color. See Colors.
outlineColor: table | numbers (optional, default opaque black) - Outline color. Read only when an argument is present after color; an explicit nil there counts as present and gives a white outline.
Measures a string with the overlay's current ImGui font, the font that draw.Text and draw.TextOutlined draw with. Use it to center or right-align labels.
Parameters:
text: string - The text to measure. Numbers are converted to strings.
Returns:
width: number - Width in pixels.
height: number - Height in pixels. Both values are 0 while the overlay has no ImGui context or font yet.
local white = { R = 1, G = 1, B = 1, A = 1 }
clover.on_render(function()
local w = clover.screen_size()
local text = "centered"local tw = draw.GetTextSize(text)
draw.TextOutlined(w / 2 - tw / 2, 40, text, white)
end)
Screen and projection
world_to_screen(x, y, z)
Projects a world position onto the screen with the game camera's current view matrix. The result is in the same pixel space as the draw functions.
Parameters:
x: number - World X.
y: number - World Y.
z: number - World Z. A Vector3 is not accepted; pass v.X, v.Y, v.Z.
Returns:
x: number - Screen x in pixels.
y: number - Screen y in pixels.
onScreen: boolean - true when the point is inside the view on both axes.
Returns 0, 0, false when the camera cannot be read (for example outside a game) or when the point is behind the camera or extremely close to it.
Points off-screen still return their coordinates, with false.
lua
local yellow = { R = 1, G = 0.9, B = 0.2, A = 1 }
clover.on_render(function()
local me = clover.local_player()
ifnot me thenreturnendlocal x, y, onScreen = world_to_screen(me.pos.x, me.pos.y, me.pos.z)
if onScreen then
draw.CircleFilled(x, y, 4, yellow)
endend)
clover.screen_size()
Returns the size of the game view in pixels, read from the engine. This is the space that world_to_screen maps into. cheat.GetWindowSize returns the same values.
Returns:
width: number - Width in pixels, or 0 when the size cannot be read.
height: number - Height in pixels, or 0 when the size cannot be read.
Drawing
Retained objects: create a Line, Text, Circle or Square once and it stays on screen until removed.
Transparency 0 is opaque
Transparency0 is opaque and 1 is invisible. Some executors use the opposite.
Objects live until removed
Objects stay until :Remove(), Drawing.clear or a VM reset. Losing your reference does not remove them.
Pixels and layering
Screen pixels, (0, 0) top-left. Higher ZIndex draws on top. Always above draw output, below the clover menu.
Creates a drawing object. It is visible right away, so set its properties immediately.
Parameters:
type: string - "Line", "Text", "Circle" or "Square" (case-insensitive). Anything else raises an error.
Returns:
object: Drawing - The new object. typeof(object) is "Drawing" and tostring(object) is "Drawing(Line)", "Drawing(Text)", "Drawing(Circle)" or "Drawing(Square)".
Drawing.clear()
Removes every drawing object created by any script. Existing handles behave like removed objects afterwards.
Aliases:cleardrawcache
Methods
Drawing:Remove()
Removes the object for good. Afterwards properties read as nil. Safe to call twice.
Aliases:Drawing:Destroy
Common properties
These exist on every type. Reading a property that does not belong to the object's type returns nil, and writing one is silently ignored (for example Radius on a Line).
Drawing.Visible
boolean, default true. Assignments use Lua truthiness, so nil hides the object. A hidden object keeps its properties and can be shown again.
Drawing.ZIndex
integer, default 0. Lower values draw first, higher values on top; equal values keep creation order. Assigning a number without an integer value (such as 1.5) or a non-number raises an error.
Drawing.Transparency
number, default 0. 0 is opaque, 1 is invisible.
Drawing.Color
Color3, default white. Also accepts a { R, G, B } table with 0-1 values. Alpha comes from Transparency.
Drawing.Thickness
number, default 1. Stroke width in pixels for lines and unfilled shapes.
Positions and sizes typed Vector2 below accept a Vector2 or a table with X/x and Y/y fields (a missing field counts as 0); any other value is silently ignored. Reading them returns a Vector2. Number properties raise an error when assigned a non-number.
Line
Property
Type
Default
Description
From
Vector2
(0, 0)
Start point in pixels.
To
Vector2
(0, 0)
End point in pixels.
Text
Property
Type
Default
Description
Text
string
""
The string drawn. Numbers are converted; other types raise an error.
Position
Vector2
(0, 0)
Top-left corner of the text, or its center when Center is true.
Size
number
13
Font size in pixels. 0 or less draws at 13.
Center
boolean
false
Centers the text both horizontally and vertically on Position.
Outline
boolean
false
Adds a 1-pixel black outline (four shifted copies) with the same opacity as the text.
TextBounds
Vector2
read-only
Rough size estimate: height is Size (13 when Size is 0 or less) and width is the byte length of Text times that height times 0.5. It is not measured from the font; writes are ignored.
Text uses the overlay's ImGui font; there is no Font property. For an exact measurement of the default-size font use draw.GetTextSize.
Circle
Property
Type
Default
Description
Position
Vector2
(0, 0)
Center in pixels.
Radius
number
50
Radius in pixels.
Filled
boolean
false
Fills the circle instead of drawing its outline.
NumSides
integer
64
Number of segments; values below 3 draw with 3. Assigning a number without an integer value raises an error.
Square
Property
Type
Default
Description
Position
Vector2
(0, 0)
Top-left corner in pixels.
Size
Vector2
(100, 100)
Width and height in pixels.
Filled
boolean
false
Fills the rectangle instead of drawing its outline.
Tweening
Drawing objects are valid targets for TweenService:Create (see Services). Number, Vector2 and Color3 properties can be tweened; ZIndex, NumSides and TextBounds cannot and raise an error.
For seconds since the Unix epoch use the global tick(), listed on Globals.
lua
local started = utility.GetTickCount()
task.delay(2, function()
print("elapsed ms", utility.GetTickCount() - started)
end)
utility.GetDeltaTime()
Returns the time in seconds between the overlay's last two rendered frames (ImGui's DeltaTime). Returns 0 while the overlay's ImGui context does not exist.
Raises Clipboard access requires Allow unsafe Lua API. while the unsafe API is off.
There is no global setclipboard or toclipboard. Use utility.SetClipboard or clover.setclipboard.
lua
local ok, err = pcall(utility.SetClipboard, "copied from clover")
ifnot ok then
warn(err)
end
*
Keyboard and mouse helpers live on input. Per-frame callbacks such as the onDraw used above are covered on cheat.
input
Read and send keyboard and mouse input.
Keys are Windows virtual-key codes
keyboard.* and clover.* key functions take virtual-key codes such as 0x41 for A (see Key codes). For Enum.KeyCode, use UserInputService on Services.
Key state ignores focus
Key state is global, so it also reports keys pressed in other apps. Check isrbxactive first.
Sent input goes to the focused window
Senders (KeyPress, Click, MoveRel, ...) go to whichever window is focused, not necessarily Roblox.
toggle-clicker.lua
local enabled = falselocal was_down = falselocal next_click = 0
cheat.register("onStep", function()
local down = keyboard.IsKeyDown(0x75)
if down andnot was_down then
enabled = not enabled
notify(enabled and"Clicker on"or"Clicker off")
end
was_down = down
local now = utility.GetTickCount()
if enabled and isrbxactive() and now >= next_click then
mouse.Click()
next_click = now + 100endend)
Press F6 (0x75) to toggle. While Roblox is focused the script clicks roughly every 100 ms; the exact spacing depends on the script loop, which runs about every 16 ms (see cheat).
Keyboard
keyboard.IsKeyDown(vk)
Returns whether the key with virtual-key code vk is held down right now.
Parameters:
vk: integer - Virtual-key code. A fractional number or a non-numeric value raises an error.
Returns the cursor position in pixels relative to the client area of the focused window, as two numbers. When no window has focus the values are screen coordinates.
Returns:
x: number - Horizontal position.
y: number - Vertical position.
Same position as mouse.GetPosition, which returns a table instead.
lua
local x, y = getmousepos()
print(string.format("cursor at %d, %d", x, y))
iskeypressed(key)
Returns whether a key or mouse button is held down. Unlike keyboard.IsKeyDown it also accepts a key name. Like the other readers it ignores focus.
Parameters:
key: integer | string - A virtual-key code, or a name from iskeypressed names. Other types raise an error.
Returns:
boolean - true while the key is down. false for an empty string or a code of 0 or less.
Unknown names use their first letter, so "F1" checks F. Use numeric codes for those keys.
"left" and "right" are the mouse buttons, not the arrow keys.
This is not clover.iskeypressed, which is keyboard.IsKeyDown and accepts only integers.
lua
if iskeypressed("shift") and iskeypressed("lmb") then
print("shift-click")
end
isrbxactive()
Returns whether the focused window belongs to the Roblox process clover is attached to.
Returns:
boolean - false when another window is focused or no process is attached.
Aliases:clover.isrbxactive
lua
if isrbxactive() and keyboard.IsKeyDown(0x45) then
print("E pressed in game")
end
Other raw functions
clover.setrobloxinput(enabled)
Accepted for compatibility with scripts that call it. The flag is stored, but nothing in the engine reads it, so the call has no effect on input. Returns nothing.
Parameters:
enabled: boolean - Any value; only its truthiness is stored.
Key codes
Common Windows virtual-key codes. Lua accepts the hex literals as written. The last column is the name the menu library's keybinds use for the same key (see ui); keyboard.* itself never takes names.
Key
Code
UI keybind name
Left mouse button
0x01
MB1
Right mouse button
0x02
MB2
Middle mouse button
0x04
MB3
Mouse button 4
0x05
Mouse4
Mouse button 5
0x06
Mouse5
Backspace
0x08
Backspace
Tab
0x09
Tab
Enter
0x0D
Enter
Shift (either)
0x10
Shift
Ctrl (either)
0x11
Ctrl
Alt (either)
0x12
Alt
Caps Lock
0x14
Escape
0x1B
Escape
Space
0x20
Space
Page Up
0x21
PageUp
Page Down
0x22
PageDown
End
0x23
End
Home
0x24
Home
Left arrow
0x25
Left
Up arrow
0x26
Up
Right arrow
0x27
Right
Down arrow
0x28
Down
Insert
0x2D
Insert
Delete
0x2E
Delete
0 to 9 (top row)
0x30 to 0x39
0 to 9
A to Z
0x41 to 0x5A
A to Z
Numpad 0 to 9
0x60 to 0x69
F1 to F12
0x70 to 0x7B
F1 to F12
Left Shift
0xA0
LeftShift
Right Shift
0xA1
RightShift
Left Ctrl
0xA2
LeftControl
Right Ctrl
0xA3
RightControl
Left Alt
0xA4
LeftAlt
Right Alt
0xA5
RightAlt
iskeypressed names
The names the global iskeypressed understands. Matching is case-insensitive.
Name
Code
Key
left, lmb
0x01
Left mouse button
right, rmb
0x02
Right mouse button
middle, mmb
0x04
Middle mouse button
tab
0x09
Tab
shift
0x10
Either Shift
ctrl, control
0x11
Either Ctrl
alt
0x12
Either Alt
escape, esc
0x1B
Escape
space
0x20
Space
insert
0x2D
Insert
delete
0x2E
Delete
any other string
first character, uppercased
Only reliable for letters and digits: a gives 0x41, 7 gives 0x37
ui
Build windows with tabs, sections and elements, with change callbacks and saved configs.
Library means the Clover global
The library is the global Clover. This page writes Library for local Library = Clover. Call methods with a colon.
An open window takes the mouse and keyboard
While any window exists, the overlay captures mouse and keyboard. Call Window:Destroy() or Library:Destroy() to release them.
Callbacks run later and cannot yield
Callbacks run on the next script pass and can't yield; use task.spawn. Errors are logged as ui callback: warnings.
SetValue is silent
SetValue, .Value = x and LoadConfig never fire callbacks. Dropdowns, keybinds and color pickers don't update on screen.
menu.lua
local Library = Clover
local Window = Library:CreateWindow({
Title = "Speed Tools",
SubTitle = "v1.0",
Size = { X = 520, Y = 400 },
})
local Main = Window:AddTab({ Title = "Main" })
local Movement = Main:AddSection("Movement")
local Enabled = Movement:AddToggle({
Id = "SpeedEnabled",
Title = "Enabled",
Default = false,
Callback = function(on)
print("speed enabled:", on)
end,
})
local Speed = Movement:AddSlider({
Id = "SpeedValue",
Title = "Speed",
Min = 16,
Max = 100,
Default = 32,
Rounding = 0,
})
Speed:OnChanged(function(value)
print("speed:", value)
end)
Movement:AddButton({
Title = "Save config",
Callback = function()
local ok = Library:SaveConfig("speed-tools")
Library:Notify({ Content = ok and"Config saved"or"Save failed", Severity = ok and"success"or"error" })
end,
})
Library:LoadConfig("speed-tools")
print(Enabled.Value, Speed.Value)
Library
Library:CreateWindow([options])
Creates a window near the top-right of the screen and returns it.
Parameters:
options: table (optional) - Window options. Keys other than the four below (for example Theme, Acrylic or TabWidth) are ignored.
options.Title: string (optional, default "clover") - Title bar text.
options.SubTitle: string (optional, default "") - Appended to the title after two spaces.
options.Size: table (optional, default { X = 580, Y = 460 }) - Size in pixels as { X = 580, Y = 460 }. A Vector2 is ignored.
options.MinSize: table (optional, default { X = 470, Y = 380 }) - Smallest size the user can resize the window to, in the same format as Size.
local Window = Clover:CreateWindow({
Title = "My Script",
SubTitle = "by me",
Size = { 600, 480 },
MinSize = { X = 500, Y = 400 },
})
Library:Notify([options])
Shows a Clover toast notification through clover.notify. Only one line of text is shown: Content when it is set, otherwise Title.
Parameters:
options: table (optional) - Notification options. Other keys, such as Duration, are ignored.
options.Content: string (optional) - Text to show. Passed through tostring.
options.Title: string (optional) - Shown only when Content is nil.
options.Severity: string (optional, default "success") - "success", "warning" or "error" (exact, lower case). Any other text is shown as success.
Aliases:Clover:Notify
lua
Clover:Notify({ Content = "Could not find the target", Severity = "warning" })
Library:SafeCallback(callback, ...)
Calls callback(...) and logs any error as a ui callback: warning instead of raising.
Parameters:
callback: function - Function to call.
...: any (optional) - Arguments passed to callback.
Aliases:Clover:SafeCallback
lua
Clover:SafeCallback(function(a, b)
print(a + b)
end, 1, 2)
Library:SaveConfig([name])
Saves every handle in Library.Options to configs/<name>.cfg in the workspace. See Configs.
Parameters:
name: string (optional, default "default") - Config name. It must match the Lua pattern ^[%w_.%-]+$ (letters, digits, _, . and -) and must not contain ...
Returns:
ok: boolean - true when the file was written; false when the name is invalid or the write was refused.
err: string - Only when the write itself failed: "file.write: refused path or unwritable file".
Aliases:Clover:SaveConfig
Everything in Library.Options is saved, including elements created by other scripts in the same VM.
Only booleans, numbers, strings and colors are saved.
lua
local ok, err = Clover:SaveConfig("legit")
ifnot ok then
warn("save failed:", err or"invalid name")
end
Library:LoadConfig([name])
Loads configs/<name>.cfg into Library.Options. Create your elements first.
Parameters:
name: string (optional, default "default") - Config name, with the same rules as SaveConfig.
Returns:
boolean - false when the name is invalid or the file cannot be read; otherwise true.
Aliases:Clover:LoadConfig
Like every SetValue, loading never calls Callback or OnChanged handlers. Read the handles' Value afterwards to apply the loaded settings.
Dropdowns, keybinds and color pickers don't update on screen.
A value of the wrong type for its element, such as text on a slider line in a hand-edited file, raises an error out of LoadConfig.
lua
if Clover:LoadConfig("legit") then
print("speed is", Clover.Options.SpeedValue.Value)
end
Library:Destroy()
Removes every script window from every script.
Aliases:Clover:Destroy
Old handles in Library.Options aren't cleared and may point at new elements. Don't reuse them.
Library:Minimize()
Does nothing. It exists so scripts written for other menu libraries do not error. Use Window:Minimize.
Aliases:Clover:Minimize
Library:Maximize()
Does nothing. It exists so scripts written for other menu libraries do not error. Use Window:Maximize.
Aliases:Clover:Maximize
Library.Options
Element handles keyed by their Id option. Shared by every script, so use unique ids.
Aliases:Clover.Options
lua
local speed = Clover.Options.SpeedValue
if speed then
print(speed.Value)
end
Library.Loaded
true once the library has loaded. Library:Destroy() sets it to false, and nothing sets it back.
Returns:
boolean
Aliases:Clover.Loaded
Library.Unloaded
false until Library:Destroy() sets it to true. Handy as a stop flag in render loops. Window:Destroy() does not change it.
Returns:
boolean
Aliases:Clover.Unloaded
Library.Version
Library version string, currently "1.0.0".
Returns:
string
Aliases:Clover.Version
Window
Library:CreateWindow returns a Window object. Tabs show in a tab bar in the order they were added; a window without tabs shows (no tabs added yet).
Window:AddTab([options])
Adds a tab to the window and returns its Tab object.
Parameters:
options: table (optional) - Tab options. Pass a table: a plain string is read as a table without Title, so the tab would be called Tab.
options.Title: string (optional, default "Tab") - Text on the tab.
options.Icon: string (optional, default "") - Stored with the tab but not drawn.
options.Content: string (optional, default "") - Body text, wrapped.
options.OkLabel: string (optional, default "OK") - OK button text. Confirm is read when OkLabel is nil.
options.CancelLabel: string (optional, default "") - Cancel button text. Cancel is read when CancelLabel is nil. When the result is empty there is no Cancel button.
options.OnOk: function (optional) - Called when OK is pressed. OnConfirm is read when OnOk is nil.
options.OnCancel: function (optional) - Called when Cancel is pressed.
Dialog callbacks are not wrapped in SafeCallback. An error in one is logged at error level as ui callback: <message>.
lua
Window:Dialog({
Title = "Reset settings?",
Content = "This puts every option back to its default.",
OkLabel = "Reset",
CancelLabel = "Cancel",
OnOk = function()
print("reset confirmed")
end,
})
Window:Minimize()
Collapses the window to its title bar until Restore or Maximize.
Window:Maximize([on])
With true, pins the window to the top-left corner at the full screen size every frame and clears the minimized state. With false, returns it to its normal, user-resizable size.
Parameters:
on: boolean (optional, default true) - Whether to maximize.
Window:Restore()
Clears both the minimized and the maximized state.
Window:SetTheme([name])
Stores a theme name on the window. The renderer does not read it, so the window keeps its fixed dark colors.
Removes this window. Its tabs and elements stop drawing and never fire again. Other windows, Library.Options, Library.Loaded and Library.Unloaded are not touched.
Array of the Tab objects returned by AddTab, in creation order.
Tabs and sections
Tabs and sections both have the Add* methods (see Elements); only tabs have AddSection. Elements added to a tab are drawn above its sections.
Tab:AddSection([title])
Adds a section to the tab and returns its Section object. Sections cannot contain other sections.
Parameters:
title: string (optional, default "") - Section title. This is a plain string, not an options table: a table is passed through tostring and shows up as table: 0x....
Returns:
Section - The new section.
lua
local Main = Window:AddTab({ Title = "Main" })
local Aim = Main:AddSection("Aim")
local Visuals = Main:AddSection("Visuals")
options.Description: string (optional, default "") - Help text and tooltip.
options.Min: number (optional, default 0) - Left end of the slider.
options.Max: number (optional, default 100) - Right end of the slider.
options.Default: number (optional, default Min) - Starting value.
options.Rounding: number (optional, default 0) - Number of decimal places shown. Dragging also rounds the value to this precision, so 0 gives whole numbers. Negative values count as 0.
options.Callback: function (optional) - Called with the new number on every change while the user drags.
Returns:
handle - Element handle whose Value is the number.
Aliases:Tab:AddSlider
Values arrive as floats (32.0, and 0.1 as 0.10000000149011612). Use math.floor for integers.
lua
Section:AddSlider({
Id = "AimFov",
Title = "FOV",
Min = 10,
Max = 500,
Default = 120,
Rounding = 0,
Callback = function(value)
print("fov", math.floor(value))
end,
})
Section:AddInput([options])
Adds a one-line text box.
Parameters:
options.Id: string (optional) - Key in Library.Options.
Adds a title line followed by wrapped text. A paragraph has no description, callback or Id (it is never added to Library.Options).
Parameters:
options.Title: string (optional, default "") - First line. Omitted when empty.
options.Content: string (optional, default "") - Wrapped body text.
Returns:
handle - Element handle whose Value is the content, with an extra SetContent method.
Aliases:Tab:AddParagraph
lua
local Status = Section:AddParagraph({ Title = "Status", Content = "Idle" })
Status:SetContent("Running")
Section:AddDropdown([options])
Adds a drop-down list. In single-select mode it shows the selected option (or (none)); in multi-select mode it shows the ticked options joined with , and each option is a checkbox.
Parameters:
options.Id: string (optional) - Key in Library.Options.
options.Description: string (optional, default "") - Help text and tooltip.
options.Default: string | number (optional) - A key name or virtual-key code. "1" means left mouse; use 49 for the 1 key.
options.Mode: string (optional, default "Toggle") - "Toggle", "Hold" or "Always". Only Toggle mode reacts to presses; Hold and Always never call Callback.
options.Callback: function (optional) - Toggle mode: called with true/false each time the key is pressed.
options.ChangedCallback: function (optional) - Called with the new key name when the user rebinds, or "" when cleared.
options.Description: string (optional, default "") - Help text and tooltip.
options.Default: Color3 | table (optional, default { R = 1, G = 1, B = 1 }) - Starting color as { R, G, B } from 0 to 1. A Color3 works too.
options.Transparency: number (optional) - 0 to 1. When set, adds an alpha bar.
options.Callback: function (optional) - Called with { R, G, B } (0 to 1). Alpha is in the handle's alpha field.
Returns:
handle - Element handle whose Value starts as { R, G, B, A } and becomes the { R, G, B } table passed to Callback. It also has SetValueRGB and alpha.
Aliases:Tab:AddColorpicker
The method is spelled AddColorpicker; AddColorPicker does not exist.
Callback receives a table, not a Color3. Build one with Color3.new(c.R, c.G, c.B) when an API needs it.
OnChanged gets the packed number, not a color table. Use Callback for colors.
lua
local Tint = Section:AddColorpicker({
Id = "EspColor",
Title = "ESP color",
Default = Color3.fromRGB(255, 80, 80),
Transparency = 0.2,
Callback = function(c)
print(c.R, c.G, c.B, Tint.alpha)
end,
})
Element handles
Every Add* method returns a handle table. The property and methods below exist on every handle; a few kinds add their own. The table shows how each kind behaves.
Kind
Value at creation
Value after user input
SetValue changes the screen
OnChanged fires
Button
nil
unchanged
no
never
Toggle
Default as a boolean
boolean
yes
on click
Slider
Default, or Min
number
yes (needs a number or numeric string)
while dragging
Input
Default text
string
yes (string or number required)
on every edit
Paragraph
Content
unchanged
yes
never
Dropdown
first option, {} when Multi
picked name, nil for (none)
no
on every pick
Keybind
Default, or ""
latched boolean (Toggle mode)
no
on each press (Toggle mode)
Colorpicker
{ R, G, B, A }
{ R, G, B }
no
on change, with a packed number
Element.Value
The handle's cached value. Reading it is the same as Get(); assigning to it calls SetValue.
lua
local Esp = Clover.Options.EspEnabled
Esp.Value = not Esp.Value
Element:Get()
Returns the cached value (see the table above).
Returns:
any
Element:SetValue(value)
Sets the value without firing callbacks. Dropdowns, keybinds and color pickers only update the cached Value.
Parameters:
value: any - New value.
Element:OnChanged(callback)
Sets the change handler (replacing any previous one) and calls it once with the current value.
Parameters:
callback: function - Called through SafeCallback with the new value.
Always false unless Mode is "Always". Track the state from Callback instead.
Returns:
boolean
Keybind:DoClick()
Calls the keybind's Callback with no arguments through SafeCallback. Neither Value nor the latch changes.
Keybind:OnClick(callback)
Same as OnChanged: the handler is called once right away with Value, then with the latched boolean on each press in Toggle mode.
Parameters:
callback: function - Handler.
Keybind.mode
The Mode option as given, "Toggle" by default.
Returns:
string
Keybind.toggled
Set to false at creation and never updated. See GetState.
Returns:
boolean
Colorpicker:SetValueRGB(color, [transparency])
Stores color as Value and sets alpha to 1 - transparency. The picker on screen does not change.
Parameters:
color: table | Color3 - Stored as is.
transparency: number (optional, default 0) - 0 to 1.
Colorpicker.alpha
Current alpha from 0 to 1. Starts at 1 - Transparency (1 without Transparency) and is updated from the picker on every change.
Returns:
number
Paragraph:SetContent(text)
Replaces the paragraph's text. Same as SetValue(tostring(text)).
Parameters:
text: any - New text, passed through tostring.
Dropdown:SetValues(list)
Stores list in the handle's _options field. The options shown on screen do not change.
Parameters:
list: table - Any value.
Internal fields
Field
Type
Description
_id
number
Element id for the clover.ui functions. 0 when creation failed because the parent tab or section no longer existed.
_kind
string
"button", "toggle", "slider", "input", "paragraph", "dropdown", "keybind" or "colorpicker".
_value
any
The storage behind Value.
_options
any
Dropdown only, set by SetValues.
Reading the live value
clover.ui.get_value(handle._id) reads the value straight from the renderer for toggles, sliders, inputs and paragraphs. See clover.ui.get_value.
Configs
Library:SaveConfig(name) writes one id=value line for each entry in Library.Options to configs/<name>.cfg inside the workspace folder, in no particular order. Values are written like this:
booleans as true or false
numbers with tostring, so 32 or 32.0
strings in double quotes, with each " written as \"; backslashes and newlines are written as they are
tables with R, G and B as color(r,g,b,a) using %g formatting, with a set to 1 when the table has no A
A window shows its tabs in a tab bar in creation order and draws only the selected tab. Inside it, elements added straight to the tab come first, then each section as a separator line with its title followed by its elements.
clover.ui.add_tab(window_id, [title], [icon])
Adds a tab to a window. An unknown window logs [clover error 10]: add_tab: window N and returns 0.
icon: string (optional, default "") - Stored but not drawn.
Returns:
tab_id: integer - New tab id, or 0.
clover.ui.add_section(tab_id, [title])
Adds a section to a tab. An unknown tab logs [clover error 11]: add_section: tab N and returns 0.
Parameters:
tab_id: integer - Parent tab.
title: string (optional, default "") - Text in the separator line.
Returns:
section_id: integer - New section id, or 0.
Elements
Each add_* takes a parent (tab or section) id and an options table, and returns the element id or 0 for an unknown parent. description shows as grey text and a tooltip.
clover.ui.add_button(parent_id, options)
Adds a full-width button. Its title is its ImGui ID, so keep button titles unique within a tab.
options.description: string (optional, default "") - Help text and tooltip.
options.min: number (optional, default 0) - Left end.
options.max: number (optional, default 100) - Right end.
options.default: number (optional, default min) - Starting value.
options.rounding: number (optional, default 0) - Decimal places shown (truncated to an integer; negative counts as 0). Dragging rounds the value to the same precision.
options.callback: function (optional) - Called with the new number on every change while dragging.
options.description: string (optional, default "") - Help text and tooltip.
options.options: table (optional, default {}) - Array of option names. At most 256 are read; non-strings are converted with tostring. Not a table means no options.
options.default: integer | string | table (optional) - Single select: a 1-based index or option name. Multi select: a table of names. Otherwise the first option, unless allow_null.
options.multi: boolean (optional, default false) - Checkbox per option.
options.searchable: boolean (optional, default false) - Adds a case-insensitive filter box (up to 63 bytes) inside the open list.
options.search_placeholder: string (optional, default "Search...") - Hint of the filter box.
options.allow_null: boolean (optional, default false) - Single select: adds a (none) entry.
options.max_items: number (optional, default 6) - Stored but not used.
options.callback: function (optional) - Single select: the picked name (nothing for (none)). Multi select: the option just toggled.
Returns:
element_id: integer
clover.ui.add_keybind(parent_id, options)
Adds a key-binding button. Click it, then press a key; Escape cancels and Delete clears.
options.description: string (optional, default "") - Help text and tooltip.
options.default: number | string (optional) - A virtual-key code or a key name. "1" means code 1 (left mouse), not the 1 key.
options.mode: string (optional, default "Toggle") - "Always" or "always", "Hold" or "hold"; anything else is Toggle. Only Toggle mode fires callback.
options.callback: function (optional) - Toggle mode: each time the key goes down an internal latch flips and this is called with its new boolean value.
options.changed_callback: function (optional) - Called with the new key name after the user rebinds, or with "" after Delete or when the key has no name in the table.
Returns:
element_id: integer
clover.ui.add_colorpicker(parent_id, options)
Adds a color picker. Callbacks get the color packed as R * 16777216 + G * 65536 + B * 256 + A (0-255 each).
options.description: string (optional, default "") - Help text and tooltip.
options.default_r: number (optional, default 1) - Red, 0 to 1.
options.default_g: number (optional, default 1) - Green, 0 to 1.
options.default_b: number (optional, default 1) - Blue, 0 to 1.
options.transparency: number (optional) - 0 (opaque) to 1. When present the picker gets an alpha bar starting at 1 - transparency; when absent alpha is fixed at 1.
options.callback: function (optional) - Called with the packed number whenever the color changes.
Returns:
element_id: integer
lua
localfunction unpack_rgba(packed)
local p = math.floor(packed)
return p // 16777216 % 256, p // 65536 % 256, p // 256 % 256, p % 256end
clover.ui.add_colorpicker(sec, {
title = "Tint",
default_r = 1,
default_g = 0.5,
default_b = 0,
callback = function(packed)
local r, g, b, a = unpack_rgba(packed)
print(r, g, b, a)
end,
})
Values and events
clover.ui.set_value(element_id, value)
Changes what an element shows. Never fires callbacks. Unknown ids are ignored.
Parameters:
element_id: integer - Target element.
value: any - Toggle: truthiness. Slider: a number. Input and paragraph: text. Ignored for other elements.
clover.ui.get_value(element_id)
Reads the value the renderer currently holds.
Parameters:
element_id: integer - Target element.
Returns:
boolean | number | string | nil - Toggle: boolean. Slider: number. Input: text. Paragraph: content. Buttons, dropdowns, keybinds, color pickers and unknown ids: nil.
clover.ui.connect_changed(element_id, callback)
Sets the element's single change handler, replacing any previous one.
Parameters:
element_id: integer - Target element.
callback: function - Handler. Anything else raises an error.
Callback arguments
Source
Arguments
When
button callback
none
click
toggle
boolean
click
slider
number
every change while dragging
input
string (full text)
every edit
dropdown, single select
option name, or none for (none)
pick
dropdown, multi select
name of the toggled option
tick or untick
keybind
boolean latch
bound key goes down, Toggle mode only
keybind changed_callback
key name, or ""
rebind or clear
colorpicker
packed RGBA number
color change
dialog ok / cancel
none
button click
Error codes
Code
Logged by
Message
10
add_tab, window_open_dialog
[clover error 10]: add_tab: window N or [clover error 10]: dialog: window N
11
add_section
[clover error 11]: add_section: tab N
13
every add_* element function
[clover error 13]: add_slider: parent N, with the calling function's name
15
VM start
A built-in Lua prelude failed to load (the one that defines Clover, or the stdlib one); the Lua error follows.
Key names
Key names accepted by a keybind's default, case-insensitive.
Name
Key
Code
A to Z
Letters
0x41 to 0x5A
0 to 9
Number row
0x30 to 0x39. Pass the code: the strings "0" to "9" are numeric, so default reads them as codes 0 to 9.
F1 to F12
Function keys
0x70 to 0x7B
LeftControl, RightControl
Left and right Ctrl
0xA2, 0xA3
LeftShift, RightShift
Left and right Shift
0xA0, 0xA1
LeftAlt, RightAlt
Left and right Alt
0xA4, 0xA5
Ctrl, Shift, Alt
Either Ctrl, Shift or Alt
0x11, 0x10, 0x12
Space
Space bar
0x20
Enter
Enter
0x0D
Tab
Tab
0x09
Backspace
Backspace
0x08
End, Home
End, Home
0x23, 0x24
Insert, Delete
Insert, Delete
0x2D, 0x2E
PageUp, PageDown
Page Up, Page Down
0x21, 0x22
Left, Up, Right, Down
Arrow keys
0x25, 0x26, 0x27, 0x28
MB1, MB2, MB3
Left, right and middle mouse button
0x01, 0x02, 0x04
Mouse4, Mouse5
Side mouse buttons
0x05, 0x06
Escape
Esc
0x1B
cheat
Register per-frame callbacks and read the Roblox viewport size.
Event names are exact
Only onDraw, onFrame, onPulse and onStep work. Any other name, including onPaint and onTick, is silently ignored.
Tick and render callbacks share one loop
Each pass (about every 16 ms) runs tick callbacks (onPulse, onStep, clover.on_tick), then render callbacks (onDraw, onFrame, clover.on_render). Draw again every pass; see draw.
An error removes the callback
A callback that errors is logged and removed. Callbacks cannot yield, so task.wait inside one errors; use task.spawn instead.
watermark.lua
local w, h = cheat.GetWindowSize()
cheat.register("onStep", function()
w, h = cheat.GetWindowSize()
end)
cheat.register("onDraw", function()
if w == 0thenreturnendlocal label = "clover"local tw, th = draw.GetTextSize(label)
draw.RectangleFilled(w - tw - 20, 10, w - 10, th + 18, 0, 0, 0, 0.6, 4)
draw.Text(w - tw - 15, 14, label, 1, 1, 1, 1)
end)
Events
Event
Phase
Same as
onPulse
tick
clover.on_tick(callback)
onStep
tick
clover.on_tick(callback)
onDraw
render
clover.on_render(callback)
onFrame
render
clover.on_render(callback)
Both names of a pair add to the same list. Within a phase, callbacks run in the order they were registered.
Loop order
One iteration of the script loop, while the master switch is on:
Scripts you queued for execution run.
Tick callbacks run.
Render callbacks run.
The engine pumps events: game events, task timers and wait resumes, tweens, RunService and UserInputServicesignals, and WebSocket messages.
The draw commands from this iteration are published to the overlay.
The thread sleeps 16 ms.
Nothing runs while the master switch is off. Callbacks get no arguments; for elapsed time use RunService.Heartbeat (see Signals). A VM reset drops all callbacks.
Functions
cheat.register(event, callback)
Adds callback to the tick or render list chosen by event. Returns nothing.
Parameters:
event: string - onPulse/onStep (tick) or onDraw/onFrame (render). Other names are ignored.
callback: function - Called with no arguments on every loop iteration. Return values are ignored.
Aliases:cheat.Register, clover.cheat_register
Raises cheat.register: expected function as argument 2 when callback is not a function.
Registering the same function twice makes it run twice per iteration.
There is no function to unregister a callback. Guard the body with a flag of your own if it needs to stop.
lua
local enabled = true
cheat.register("onDraw", function()
ifnot enabled thenreturnend
draw.Text(20, 20, "hello from onDraw", 1, 1, 1, 1)
end)
cheat.GetWindowSize()
Returns the width and height of the Roblox viewport in pixels, read from the game's render engine. Can be called anywhere, not only in callbacks.
Returns:
width: number - Viewport width, or 0 when it cannot be read (for example, not attached yet).
height: number - Viewport height, or 0 when it cannot be read.
Read and write files inside the clover workspace folder.
Everything lives in the workspace
Paths are relative to C:\clover\workspace. Absolute paths and .. are refused. No unsafe API needed. See Path rules.
Two error styles
file.Read/file.Write return nil/false plus a message on failure. The globals (readfile, writefile, ...) raise errors instead, so use pcall.
16 MiB per file
Reads fail for files larger than 16 MiB (16777216 bytes) and writes fail for data larger than that. appendfile fails when the combined size would exceed it.
settings.lua
local path = "myscript/settings.json"local settings = { esp = true, fov = 90 }
if isfile(path) thenlocal ok, data = pcall(function()
return HttpService:JSONDecode(readfile(path))
end)
if ok and type(data) == "table"then
settings = data
endend
settings.runs = (settings.runs or0) + 1
writefile(path, HttpService:JSONEncode(settings))
print("saved", path, "run", settings.runs)
writefile creates the myscript folder on the first run. JSON helpers are on http.
file table
file.Read(path)
Reads a whole file. Binary-safe: the string holds the exact bytes.
Parameters:
path: string - Workspace-relative path. A trailing slash makes it invalid.
Returns:
data: string - The file contents on success.
nil - On failure nil is returned together with the message file.read: refused path or unreadable file. This covers a refused path, a missing file, a folder and a file over 16 MiB.
Aliases:file.read, clover.file_read
lua
local text, err = file.Read("notes.txt")
if text then
print(#text, "bytes")
else
warn(err)
end
file.Write(path, data)
Creates or overwrites a file with data. Missing parent folders are created.
Parameters:
path: string - Workspace-relative path. A trailing slash makes it invalid.
data: string - New contents, binary-safe. Numbers are converted to strings; other types raise an error.
Returns:
boolean - true on success. On failure false is returned together with the message file.write: refused path or unwritable file.
Aliases:file.write, clover.file_write
lua
local ok, err = file.Write("logs/session.txt", "started")
ifnot ok then
warn(err)
end
file.Exists(path)
Returns whether path names an existing regular file. Folders return false; use isfolder for those.
Parameters:
path: string - Workspace-relative path.
Returns:
boolean - true for an existing file, false for folders, missing files and refused paths.
boolean - true when the file was deleted, false otherwise.
Aliases:file.delete, clover.file_delete
Global functions
These globals are also on the clover table (clover.readfile and so on). Unlike the file table they strip trailing / and \ from the path first, so "logs/" means "logs".
readfile(path)
Reads a whole file. Binary-safe.
Parameters:
path: string - Workspace-relative path.
Returns:
string - The file contents.
Aliases:clover.readfile
Raises readfile: the file does not exist or the path is not allowed for a refused path, a missing file, a folder or a file over 16 MiB.
lua
local ok, text = pcall(readfile, "notes.txt")
print(ok and text or"no notes yet")
writefile(path, data)
Creates or overwrites a file with data. Missing parent folders are created. Returns nothing.
Parameters:
path: string - Workspace-relative path.
data: string - New contents, binary-safe. Numbers are converted to strings.
Aliases:clover.writefile
Raises writefile: the path is not allowed or the file cannot be written on failure, including data over 16 MiB.
Adds data to the end of a file, creating the file (and missing parent folders) when it does not exist. The existing contents are read and the whole file is written again. Returns nothing.
Parameters:
path: string - Workspace-relative path.
data: string - Text or bytes to append. Numbers are converted to strings.
Aliases:clover.appendfile
Raises an error if the path is refused, the file can't be written, or the result would exceed 16 MiB.
Returns whether path names an existing regular file. Same check as file.Exists, after trailing slashes are stripped.
Parameters:
path: string - Workspace-relative path.
Returns:
boolean - true for an existing file, false for folders, missing files and refused paths.
Aliases:clover.isfile
isfolder(path)
Returns whether path names an existing folder.
Parameters:
path: string - Workspace-relative path.
Returns:
boolean - true for an existing folder, false for files, missing folders and refused paths.
Aliases:clover.isfolder
makefolder(path)
Creates a folder together with any missing parent folders. Succeeds without changes when the folder already exists. Returns nothing.
Parameters:
path: string - Workspace-relative folder path.
Aliases:clover.makefolder
Raises makefolder: the path is not allowed or the folder cannot be created on failure, for example when a file already has that name.
lua
ifnot isfolder("myscript/cache") then
makefolder("myscript/cache")
end
delfile(path)
Deletes a file. Returns nothing.
Parameters:
path: string - Workspace-relative path of a file.
Aliases:clover.delfile
Raises delfile: the file does not exist or the path is not allowed on failure, including when path is a folder.
There is no delfolder: folders cannot be deleted from Lua.
lua
if isfile("myscript/old.txt") then
delfile("myscript/old.txt")
end
listfiles(path)
Lists the files and folders directly inside a folder. Pass "" to list the workspace root.
Parameters:
path: string - Workspace-relative folder path, or "" for the root. Required.
Returns:
table - An array of paths that can be passed straight to the other functions: path .. "/" .. name for each entry, or just the name when listing the root. Folders have no trailing slash. The order is whatever Windows returns.
Aliases:clover.listfiles
At most 10000 entries are returned. Symbolic links, junctions and names that break the path rules are skipped.
Raises listfiles: the folder does not exist or the path is not allowed on failure.
lua
for _, entry in ipairs(listfiles("")) doif isfolder(entry) then
print("folder", entry)
else
print("file", entry)
endend
Path rules
Every function checks the path the same way before touching the disk. A path that breaks a rule is refused: the file table returns nil/false, isfile/isfolder return false and the other globals raise an error.
Paths are relative to the workspace root. Both / and \ work as separators.
Absolute paths are refused: no drive letter (C:...) and no leading / or \.
The path must not be empty, must be valid UTF-8, must not contain a NUL byte and must be at most 4096 bytes long.
No segment may be . or .., so neither ../x nor ./x works.
No segment may end in a dot or a space, or contain :, *, ?, ", <, >, | or a control character.
A trailing slash leaves an empty last segment, which the file table refuses. The globals strip trailing slashes before checking.
Windows device names are refused as a segment, compared case-insensitively against the part before the first dot: CON, PRN, AUX, NUL, CONIN$, CONOUT$, CLOCK$, COM0 to COM9 and LPT0 to LPT9. So nul.txt is refused too.
Symbolic links and junctions are refused anywhere along the path, and so are files with more than one hard link.
Limits
Limit
Value
File size for reads and writes
16 MiB (16777216 bytes)
Path length
4096 bytes
Entries returned by listfiles
10000
loadfile and dofile
loadfile(path) and dofile(path) are Lua shims built on readfile, so they follow the same path rules. loadfile returns the compiled chunk, or nil and a message; dofile runs the file and raises on failure. Details are on Executor Compatibility.
The menu library's SaveConfig and LoadConfig also store their files here, under configs/. See ui.
task
Sleep, spawn, delay and defer coroutines.
Waiting needs a coroutine
Only top-level code and functions started by task.spawn/delay/defer can wait. Engine callbacks (signals, render, UI) can't; spawn a task inside them.
Extra arguments are dropped
Extra arguments after the function are ignored. Capture values in a closure.
Time advances in ticks
Ticks are about 16 ms, so timing is accurate to roughly one tick.
task-demo.lua
local loop = task.spawn(function()
whiletruedo
print("tick", task.wait(1))
endend)
local reminder
reminder = task.delay(10, function()
reminder = nil
notify("ten seconds passed")
end)
task.defer(function()
print("runs on the next scheduler pass")
end)
task.wait(3)
task.cancel(loop)
if reminder then
reminder:Cancel()
reminder = nilend
print("loop stopped, reminder cancelled")
How scheduling works
Every script shares one Lua state that runs on a single script thread. That thread repeats the steps below, then sleeps about 16 ms. dt is the real time in seconds since the previous tick started. Nothing runs while the master switch is off.
Scripts queued since the last tick start, each in a new coroutine, and run until they finish or first yield.
clover.on_tick callbacks run, then clover.on_render callbacks (see cheat).
game and workspace are refreshed if the DataModel changed, and polled game events fire: PlayerAdded, PlayerRemoving, CharacterAdded, ChildAdded, ChildRemoved and DescendantAdded.
dt is subtracted from every sleeping thread's remaining time. Threads that reach zero resume, and their wait returns dt.
Due task.delay and task.defer functions start, each in a new coroutine.
Playing tweens write their new values (see TweenService).
RunService.Heartbeat, RenderStepped and Stepped fire with dt, and threads waiting on them resume.
UserInputService.InputBegan and InputEnded fire for key and mouse button changes.
WebSocket OnMessage and OnClose events fire.
Queued UI element callbacks run and the frame's draw list is published.
Code that runs without yielding holds the script thread: no other script, callback or signal runs until it yields or returns. A VM reset or turning the master switch off interrupts it. A VM reset also drops every sleeping thread, pending delay, signal connection and tween.
Where you can yield
Code running in
Can yield
The top-level script chunk
Yes
Functions started by task.spawn, spawn or Spawn
Yes
Functions started by task.delay, delay, Delay or task.defer
Yes
Tween.Completed and game.Loaded handlers (the engine starts them with task.spawn)
Yes
Engine signal handlers (:Connect and :Once on RunService, UserInputService, Instance, Player and WebSocket signals), including RunService:BindToRenderStep and ContextActionService:BindAction callbacks
No
clover.on_tick, clover.on_render and cheat.register callbacks
No
UI element callbacks
No
Functions
The globals wait and spawn are the same C functions as task.wait and task.spawn. Wait, Spawn, delay and Delay are defined by the standard prelude on top of the task functions. There is no global defer or cancel.
task.wait([seconds])
Pauses the current coroutine for seconds.
Yields.
Parameters:
seconds: number (optional, default 0.03) - Time to sleep, in seconds.
Returns:
dt: number - Length in seconds of the tick that resumed the thread. This is not the total time slept.
Aliases:wait, Wait
Raises an error when the calling code cannot yield (see Where you can yield).
task.cancel removes a sleeping thread so that it never resumes.
lua
for i = 3, 1, -1do
print(i)
task.wait(1)
end
print("go")
task.spawn(callback)
Runs callback in a new coroutine right away. Use it for loops that wait, or to wait from a callback.
Parameters:
callback: function - Called with no arguments. Coroutines are not accepted.
Returns:
thread: thread - The new coroutine. Pass it to task.cancel to stop it while it sleeps.
Aliases:spawn, Spawn
Arguments after callback are ignored.
An error inside the coroutine, on the first run or after a later resume, is caught and logged as task: <message>. It never reaches the caller.
lua
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.G then
task.spawn(function()
print("charging")
task.wait(2)
print("released")
end)
endend)
task.delay(seconds, callback)
Schedules callback to start in a new coroutine once seconds of scheduler time have passed. The coroutine may yield.
Parameters:
seconds: number - Delay in seconds. Required; negative values count as 0.
callback: function - Called with no arguments.
Returns:
handle: Task - A Task handle that can cancel the call before it runs.
Aliases:delay, Delay
Arguments after callback are ignored.
Errors in callback are logged as task: <message>.
lua
local hint
hint = task.delay(5, function()
hint = nil
notify("Press F to open the menu")
end)
UserInputService.InputBegan:Connect(function(input)
if hint and input.KeyCode == Enum.KeyCode.F then
hint:Cancel()
hint = nilendend)
task.defer(callback)
Queues callback with a delay of zero. It starts in a new coroutine the next time the scheduler runs due delays, which is within one tick, and never before task.defer returns.
Cancels a pending task.delay/task.defer handle, or a coroutine sleeping in task.wait or WaitForChild.
Parameters:
target: Task | thread - A handle or a coroutine. Any other value is ignored.
Returns nothing and never raises an error for a wrong type.
A coroutine that is running, finished, or waiting on a signal's :Wait() is not affected.
The coroutine is not closed; it simply stays suspended.
lua
local counter = task.spawn(function()
local n = 0whiletruedo
n = n + 1
print("count", n)
task.wait(0.5)
endend)
task.wait(2)
task.cancel(counter)
print("counter stopped")
Task handle
task.delay and task.defer return a Task handle, a userdata for which typeof returns "Task". It has one method.
Cancel only while pending
Cancelling a handle twice, or after it ran, can cancel an unrelated delay. Drop the reference once it's done.
Task:Cancel()
Same as task.cancel(handle): removes the pending call so it never runs. Returns nothing.
lua
local pending = task.delay(3, function()
print("this never prints")
end)
pending:Cancel()
pending = nil
Signals
Event objects with Connect, Once, Wait and Fire, and every signal the engine exposes.
Handlers cannot yield
Handlers can't yield. Use task.spawn for anything that waits (see task).
A handler that raises an error is disconnected
The error is logged as signal: <message> and that connection is removed, so the handler is never called again. Wrap risky code in pcall if the handler must survive errors.
Call methods with a colon
Connect, Once and Fire read their arguments from position 2. signal:Connect(fn) works; signal.Connect(fn) raises an error and signal.Fire(a) drops a.
signals-demo.lua
local elapsed = 0local heartbeat = RunService.Heartbeat:Connect(function(dt)
elapsed = elapsed + dt
end)
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print("joined:", player.Name)
end)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if input.KeyCode == Enum.KeyCode.F then
print("F pressed after", elapsed, "seconds")
endend)
local dt = RunService.Heartbeat:Wait()
print("first tick took", dt)
task.wait(10)
heartbeat:Disconnect()
heartbeat = nil
Signal object
Engine signals are plain tables holding the four functions below, so typeof returns "table" for them. Instance signals such as part.ChildAdded build a new table every time you read them; connections made through any copy behave the same.
Signal:Connect(callback)
Registers callback. Each time the signal fires, the handler is called with the signal's arguments. Handlers run one after another on the script thread, in no guaranteed order.
Parameters:
callback: function - The handler. Raises an error if this is not a function.
Returns:
connection: RBXScriptConnection - Use it to disconnect the handler.
Handlers connected during a fire run from the next fire.
Connections last until they are disconnected or the VM resets; they do not end when the script that made them finishes.
lua
local total = 0
RunService.Heartbeat:Connect(function(dt)
total = total + dt
end)
Signal:Once(callback)
Like Connect, but the connection is removed just before the handler's first call.
Parameters:
callback: function - The handler.
Returns:
connection: RBXScriptConnection - Can disconnect the handler before it runs.
Suspends the running coroutine until the signal next fires, then returns the values it fired with.
Yields.
Returns:
...any - The fire's arguments, for example dt for RunService.Heartbeat.
Raises an error when the calling code cannot yield.
There is no timeout. The coroutine stays suspended until the next fire or a VM reset, and task.cancel does not remove it.
lua
local character = game:GetService("Players").LocalPlayer.CharacterAdded:Wait()
print("respawned as", character.Name)
Signal:Fire(...)
Fires the signal from Lua. Every matching handler is called with the given values and every coroutine in :Wait() resumes with them, all before Fire returns.
Parameters:
...: any (optional) - Values passed to the handlers.
RunService.Heartbeat, RenderStepped, Stepped and their aliases are one event, so firing any of them runs the handlers of all of them.
A property-changed signal only reaches handlers for the same instance and property name.
This reaches handlers inside this engine only. It does not fire the real Roblox event; for that see firesignal on the Remotes page.
lua
local storage = game:GetService("ReplicatedStorage")
local remote = storage and storage:FindFirstChild("Notify")
if remote and remote:IsA("RemoteEvent") then
remote.OnClientEvent:Connect(function(message)
print("handler got", message)
end)
remote.OnClientEvent:Fire("test message")
end
Connection object
Connect and Once on engine signals return an RBXScriptConnection userdata (typeof returns "RBXScriptConnection"). It has a single method and no Connected property: reading connection.Connected returns nil.
Disconnect once
Disconnected slots are reused, so calling Disconnect twice can disconnect a newer connection. Clear your reference after disconnecting.
RBXScriptConnection:Disconnect()
Stops the handler from being called again and releases it. Returns nothing.
lua
local conn
conn = RunService.Heartbeat:Connect(function()
if UserInputService:IsKeyDown(Enum.KeyCode.Escape) then
conn:Disconnect()
conn = nilendend)
Lua-side signals
A few signals are written in Lua instead of being engine signals. They use the same method names but behave differently:
Caught by pcall and silently ignored; the handler stays connected
Polls with task.wait() until the next Fire, then returns its values
:Fire(...) calls handlers in connection order. The engine never fires these
Tween.Completed
A table with Connected and Disconnect
Reported with warn as Tween completion: <message>; handlers run in their own coroutine and may yield
Yields until the tween completes or is cancelled and returns the PlaybackState; raises an error outside a coroutine
Not available
game.Loaded
A table whose Connected is false and whose Disconnect does nothing
The handler starts at once in task.spawn, so errors are logged as task: <message>
Returns immediately with no values
Not available
Connection.Connected
true until disconnected. Only on Lua-side signals; engine connections don't have it.
Returns:
boolean
Signal reference
Every signal the engine exposes, with the arguments handlers receive in order. Game events are found by comparing snapshots once per tick, so a change that comes and goes within one tick is not reported.
Signal
Fires when
Arguments
Notes
RunService.Heartbeat
Once per scheduler tick, after sleeping threads, delays and tweens are processed
dt: number, seconds since the previous tick
Alias RunService.PostSimulation
RunService.RenderStepped
At the same moment as Heartbeat; it is the same event
dt
Alias RunService.PreRender. Not tied to Roblox render frames
RunService.Stepped
At the same moment as Heartbeat
dt
Aliases RunService.PreAnimation and RunService.PreSimulation. Roblox passes (time, dt); here only dt
UserInputService.InputBegan
A key with a KeyCode mapping, or mouse button 1, 2 or 3, goes down while Roblox is the foreground window
Events not listed here, like Humanoid.Died or Touched, don't exist.
Property-changed signals
Instance:GetPropertyChangedSignal(property)
Returns a signal that fires, with no arguments, right after this engine assigns property on this instance. It fires on every such assignment, even when the value did not change.
Parameters:
property: string - Property or attribute name.
Returns:
Signal - An engine signal bound to this instance and name.
Any instance: Parent (only when the reparent succeeds) and Value.
Any name passed to instance:SetAttribute(name, value) on the same instance, including when the value is nil.
Writes that the game makes on its own are never detected.
lua
local character = game:GetService("Players").LocalPlayer.Character
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid then
humanoid:GetPropertyChangedSignal("WalkSpeed"):Connect(function()
print("WalkSpeed set to", humanoid.WalkSpeed)
end)
humanoid.WalkSpeed = 24end
BindableEvent and ProximityPrompt
BindableEvent:Fire(...)
Fires the instance's Event (or Triggered) for your own handlers. Roblox scripts aren't notified.
Parameters:
...: any (optional) - Values passed to the handlers.
Aliases:ProximityPrompt:Fire
Raises Fire: unsupported instance on any other class.
lua
local storage = game:GetService("ReplicatedStorage")
local bindable = storage and storage:FindFirstChildOfClass("BindableEvent")
if bindable then
bindable.Event:Connect(function(a, b)
print("got", a, b)
end)
bindable:Fire("hello", 42)
end
Enum
Roblox-style enumerations used by input, RunService and TweenService.
clover only defines the enums listed on this page. Indexing a group or item that does not exist returns nil instead of raising an error, so check for typos when a comparison never matches.
enums.lua
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F then
print("pressed", input.KeyCode.Name, input.KeyCode.Value)
endend)
print(tostring(Enum.KeyCode.Space))
print(Enum.KeyCode:FromValue(97))
print(#Enum.UserInputType:GetEnumItems())
EnumItem
Every item is a unique table, so compare items with ==. Items of the groups built by the standard library (all groups except the three tween groups) also convert to strings like Enum.KeyCode.Space.
EnumItem.Name
The item name, for example "Space".
Returns:
string
EnumItem.Value
The numeric value. KeyCode values follow Roblox, not Windows virtual-key codes.
Returns:
integer
EnumItem.EnumType
The group table the item belongs to, for example Enum.KeyCode. Not set on the tween enums.
Returns:
table
Enum groups
Groups built by the standard library answer these three lookups. Call them with a colon. tostring(Enum.KeyCode) returns "KeyCode".
Enum.<Group>:GetEnumItems()
Returns a new array with every item of the group, in definition order.
Returns:
{EnumItem}
Enum.<Group>:FromValue(value)
Looks an item up by its numeric value. When two names share a value, the one defined last wins.
Parameters:
value: integer
Returns:
EnumItem? - The item, or nil.
Enum.<Group>:FromName(name)
Looks an item up by name. Same as Enum.<Group>[name].
Parameters:
name: string
Returns:
EnumItem? - The item, or nil.
KeyCode
Windows VK is the key code clover polls. Keys marked none exist only for compatibility and are never reported. See Services.
Name
Value
Windows VK
A ... Z
97 ... 122
65 ... 90
Zero ... Nine
48 ... 57
48 ... 57
KeypadZero ... KeypadNine
256 ... 265
96 ... 105
F1 ... F15
282 ... 296
112 ... 126
Unknown
0
none
Backspace
8
8
Tab
9
9
Clear
12
none
Return
13
13
Pause
19
19
Escape
27
27
Space
32
32
QuotedDouble
34
none
Hash
35
none
Dollar
36
none
Percent
37
none
Ampersand
38
none
Quote
39
222
LeftParenthesis
40
none
RightParenthesis
41
none
Asterisk
42
none
Plus
43
none
Comma
44
188
Minus
45
189
Period
46
190
Slash
47
191
Colon
58
none
Semicolon
59
186
LessThan
60
none
Equals
61
187
GreaterThan
62
none
Question
63
none
At
64
none
LeftBracket
91
219
BackSlash
92
220
RightBracket
93
221
Caret
94
none
Underscore
95
none
Backquote
96
192
LeftCurly
123
none
Pipe
124
none
RightCurly
125
none
Tilde
126
none
Delete
127
46
KeypadPeriod
266
110
KeypadDivide
267
111
KeypadMultiply
268
106
KeypadMinus
269
109
KeypadPlus
270
107
KeypadEnter
271
none
KeypadEquals
272
none
Up
273
38
Down
274
40
Right
275
39
Left
276
37
Insert
277
45
Home
278
36
End
279
35
PageUp
280
33
PageDown
281
34
NumLock
300
144
CapsLock
301
20
ScrollLock
302
145
RightShift
303
161
LeftShift
304
160
RightControl
305
163
LeftControl
306
162
RightAlt
307
165
LeftAlt
308
164
RightMeta
309
none
LeftMeta
310
none
LeftSuper
311
91
RightSuper
312
92
Mode
313
none
Compose
314
none
Help
315
none
Print
316
44
SysReq
317
none
Break
318
none
Menu
319
93
Power
320
none
Euro
321
none
Undo
322
none
UserInputType
Name
Value
Notes
MouseButton1
0
Left button, reported from VK 1.
MouseButton2
1
Right button, reported from VK 2.
MouseButton3
2
Middle button, reported from VK 4.
MouseWheel
3
Defined only.
MouseMovement
4
Defined only.
Touch
7
Defined only.
Keyboard
8
Type of every keyboard input object.
Focus
9
Defined only.
Accelerometer
10
Defined only.
Gyro
11
Defined only.
Gamepad1
12
Defined only.
TextInput
14
Defined only.
InputMethod
15
Defined only.
None
16
Defined only.
Other groups
Group
Items (value)
UserInputState
Begin (0), Change (1), End (2), Cancel (3), None (4)
First (0), Input (100), Camera (200), Character (300), Last (2000)
RaycastFilterType
Exclude (0), Include (1), Blacklist (0), Whitelist (1)
ContextActionResult
Pass (1), Sink (0)
Tween enums
Created by TweenService. Items are plain tables with Name and Value only, without the group methods. TweenService also accepts names as strings, like "Quad".
Only new, zero, one, X/x, Y/y, Magnitude, Unit and arithmetic exist. No methods like Dot or Lerp.
mouse-ring.lua
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local ring = Drawing.new("Circle")
ring.Radius = 60
ring.Thickness = 1
ring.Color = Color3.fromRGB(255, 255, 255)
local line = Drawing.new("Line")
line.Color = Color3.fromRGB(255, 80, 80)
line.Thickness = 2
RunService.RenderStepped:Connect(function()
local width, height = clover.screen_size()
local center = Vector2.new(width / 2, height / 2)
local mouse = UserInputService:GetMouseLocation()
local delta = mouse - center
ring.Position = mouse
line.From = center
line.To = center + delta.Unit * math.min(delta.Magnitude, 200)
line.Visible = delta.Magnitude > ring.Radius
end)
Constructors
Vector2.new([x], [y])
Creates a new Vector2 from two numbers. Missing or nil components are 0.
Parameters:
x: number (optional, default 0) - Horizontal component. Numeric strings such as "5" are converted; any other non-nil value raises number expected.
y: number (optional, default 0) - Vertical component, same rules as x.
Returns:
Vector2 - A new vector. Components are stored as 32-bit floats.
Can't copy a vector: Vector2.new(v) raises an error.
lua
local size = Vector2.new(200, 40)
local origin = Vector2.new()
print(size, origin, Vector2.new(7))
Constants
Constants are plain fields of the global Vector2 table, created once when the script state starts. Every read returns the same immutable object.
Vector2.zero
The vector 0, 0.
Returns:
Vector2
Vector2.one
The vector 1, 1.
Returns:
Vector2
Properties
Vector2.X
The horizontal component. It always comes back as a Lua float (math.type gives float), so print(Vector2.new(3, 4).X) prints 3.0.
Read-only.
Returns:
number
Aliases:Vector2.x
Vector2.Y
The vertical component, returned as a Lua float.
Read-only.
Returns:
number
Aliases:Vector2.y
Vector2.Magnitude
The length of the vector, sqrt(X * X + Y * Y), computed in single precision.
Read-only.
Returns:
number
lua
local v = Vector2.new(3, 4)
print(v.X, v.y, v.Magnitude)
Vector2.Unit
The same direction with length 1, or 0, 0 for a zero vector.
Read-only.
Returns:
Vector2
lua
print(Vector2.new(3, 4).Unit, Vector2.zero.Unit)
Operators
Expression
Result
Behavior
a + b
Vector2
Component-wise sum. Both operands must be Vector2: v + 1 raises clover.Vector2 expected, got number.
a - b
Vector2
Component-wise difference, with the same operand rule as +.
v * n, n * v
Vector2
Scales both components by the number n. Multiplying two vectors raises number expected.
v / n
Vector2
Divides both components by n. n / v raises an error. Dividing by 0 gives inf or nan components instead of an error.
-v
Vector2
Negates both components.
a == b
boolean
True when both components are exactly equal as 32-bit floats. Comparing with a different userdata type, such as a Vector3, raises an error; comparing with nil, a number or a table is just false.
tostring(v)
string
The components formatted with %g (6 significant digits) and joined by a comma and space, for example 0.333333, 1e+10. print(v) uses the same text.
typeof(v)
string
Vector2. Plain type(v) returns userdata.
No other operator is defined. //, %, ^, <, # and .. raise the usual Lua errors, so call tostring(v) before concatenating a vector into a string.
Storage
Each Vector2 is a full userdata holding two 32-bit floats, with the metatable named clover.Vector2. That name is what appears in error messages, and typeof strips the clover. prefix.
Components are rounded to single precision: Vector2.new(0.1, 0).X reads back as 0.10000000149012. Compare results of arithmetic with a tolerance instead of ==.
Values are immutable. There is no __newindex, so v.X = 5 raises attempt to index a clover.Vector2 value. Build a new vector instead.
Every constructor call and operator allocates a new userdata. Two equal vectors are still different table keys, so t[Vector2.new(1, 2)] does not find an entry stored under another Vector2.new(1, 2).
Where Vector2 is used
Drawing objects return Vector2 for Line From and To, Position on Text, Circle and Square, Square Size and the read-only Text TextBounds. Setting From, To, Position or Square Size accepts a Vector2 or a plain table with X and Y (or x and y) fields, where a missing field counts as 0.
UserInputService:GetMouseLocation() and UserInputService:GetMouseDelta() return Vector2 values built from the mouse position (see services).
Remote calls can send Vector2 arguments and receive Vector2 results (see remotes).
TweenService can tween Vector2 values (see services).
X, Y, Z are case-insensitive; Magnitude, Unit, Dot, Cross and Lerp aren't. Max, Min, Abs and similar don't exist.
distance-to-players.lua
local Players = game:GetService("Players")
local me = Players.LocalPlayer
localfunction rootOf(player)
local character = player.Character
return character and character:FindFirstChild("HumanoidRootPart")
endlocal myRoot = me and rootOf(me)
if myRoot thenfor _, player in ipairs(Players:GetPlayers()) dolocal root = rootOf(player)
if player ~= me and root thenlocal delta = root.Position - myRoot.Position
local flat = Vector3.new(delta.X, 0, delta.Z)
print(player.Name, math.floor(delta.Magnitude), "studs, direction", flat.Unit)
endendend
Constructors
Vector3.new([x], [y], [z])
Creates a new Vector3 from three numbers. Missing or nil components are 0.
If the first argument is already a Vector3, it returns a copy of that vector and ignores the other arguments.
Parameters:
x: number | Vector3 (optional, default 0) - X component, or a Vector3 to copy. Numeric strings such as "5" are converted; any other non-nil value raises number expected.
y: number (optional, default 0) - Y component, same rules as x.
z: number (optional, default 0) - Z component, same rules as x.
Returns:
Vector3 - A new vector. Components are stored as 32-bit floats.
lua
local a = Vector3.new(1, 2, 3)
local b = Vector3.new(a)
print(a == b, rawequal(a, b), Vector3.new(5))
Constants
Constants are plain fields of the global Vector3 table, created once when the script state starts. Every read returns the same immutable object.
Vector3.zero
The vector 0, 0, 0.
Returns:
Vector3
Vector3.one
The vector 1, 1, 1.
Returns:
Vector3
Vector3.xAxis
The unit vector 1, 0, 0.
Returns:
Vector3
Vector3.yAxis
The unit vector 0, 1, 0, which is up in Roblox world space.
Returns:
Vector3
Vector3.zAxis
The unit vector 0, 0, 1.
Returns:
Vector3
Properties
Vector3.X
The X component. It always comes back as a Lua float (math.type gives float), so print(Vector3.new(1, 2, 3).X) prints 1.0.
Read-only.
Returns:
number
Aliases:Vector3.x
Vector3.Y
The Y component, returned as a Lua float.
Read-only.
Returns:
number
Aliases:Vector3.y
Vector3.Z
The Z component, returned as a Lua float.
Read-only.
Returns:
number
Aliases:Vector3.z
Vector3.Magnitude
The length of the vector, sqrt(X * X + Y * Y + Z * Z), computed in single precision.
Read-only.
Returns:
number
lua
local a = Vector3.new(0, 0, 0)
local b = Vector3.new(3, 4, 0)
print((b - a).Magnitude)
Vector3.Unit
The same direction with length 1, or 0, 0, 0 for a zero vector.
Read-only.
Returns:
Vector3
Methods
Methods are only reachable through a vector, as v:Dot(w) or v.Dot(v, w). Vector3.Dot on the global table is nil. Every argument named other or goal must be a Vector3; anything else, including a Vector2, raises clover.Vector3 expected.
Vector3:Dot(other)
Returns the dot product X * other.X + Y * other.Y + Z * other.Z.
Parameters:
other: Vector3 - The second vector.
Returns:
number - The dot product. For two unit vectors it is the cosine of the angle between them.
lua
local look = Vector3.new(0, 0, -1)
local toTarget = Vector3.new(3, 0, -4).Unit
print(look:Dot(toTarget))
Vector3:Cross(other)
Returns the cross product, a vector perpendicular to both inputs. It follows the right-hand rule: Vector3.xAxis:Cross(Vector3.yAxis) is 0, 0, 1.
Parameters:
other: Vector3 - The second vector.
Returns:
Vector3 - Y * other.Z - Z * other.Y, Z * other.X - X * other.Z, X * other.Y - Y * other.X.
Vector3:Lerp(goal, alpha)
Linear interpolation: returns self + (goal - self) * alpha.
Parameters:
goal: Vector3 - The vector reached at alpha = 1.
alpha: number - Required. It is not clamped, so values below 0 or above 1 extrapolate past the endpoints.
Returns:
Vector3 - The interpolated vector.
lua
local a = Vector3.new(0, 0, 0)
local b = Vector3.new(10, 0, 0)
print(a:Lerp(b, 0.25), a:Lerp(b, 2))
Operators
Expression
Result
Behavior
a + b
Vector3
Component-wise sum. Both operands must be Vector3: v + 1 raises clover.Vector3 expected, got number.
a - b
Vector3
Component-wise difference, with the same operand rule as +.
v * n, n * v
Vector3
Scales every component by the number n. Multiplying two vectors raises number expected; use Dot or Cross instead.
v / n
Vector3
Divides every component by n. n / v raises an error. Dividing by 0 gives inf or nan components instead of an error.
-v
Vector3
Negates every component.
cf * v
Vector3
A CFrame on the left transforms the vector from the frame's local space into world space. The reverse order, v * cf, raises number expected.
a == b
boolean
True when all three components are exactly equal as 32-bit floats. Comparing with a different userdata type, such as a Vector2, raises an error; comparing with nil, a number or a table is just false.
tostring(v)
string
The components formatted with %g (6 significant digits) and joined by a comma and space, for example 1.23457e+08, 1e-07, 0.333333. print(v) uses the same text.
typeof(v)
string
Vector3. Plain type(v) returns userdata.
No other operator is defined. //, %, ^, <, # and .. raise the usual Lua errors, so call tostring(v) before concatenating a vector into a string.
Storage
Each Vector3 is a full userdata holding three 32-bit floats, with the metatable named clover.Vector3. That name is what appears in error messages, and typeof strips the clover. prefix.
Components are rounded to single precision: Vector3.new(0.1).X reads back as 0.10000000149012. Compare results of arithmetic with a tolerance, for example (a - b).Magnitude < 1e-4, instead of ==.
Values are immutable. There is no __newindex, so v.X = 5 raises attempt to index a clover.Vector3 value. Build a new vector instead.
Every constructor call and operator allocates a new userdata. Two equal vectors are still different table keys, so t[Vector3.new(1, 2, 3)] does not find an entry stored under another Vector3.new(1, 2, 3).
Where Vector3 is used
Instance properties: Position, Size, Velocity (or AssemblyLinearVelocity) and AssemblyAngularVelocity on parts, and Value on a Vector3Value. Assigning Position, Size, Velocity, AssemblyLinearVelocity or a Vector3Value's Value accepts a Vector3 or a plain table with X, Y and Z (or lowercase) fields, where a missing field counts as 0.
CFramePosition, LookVector, RightVector and UpVector are Vector3 values, and CFrame.new accepts a Vector3 position.
Input objects passed to UserInputService.InputBegan and InputEnded handlers carry Position as Vector3.new(mouseX, mouseY, 0) and a zero Delta (see services).
SetAttribute accepts a Vector3 value (see attributes).
Remote calls can send Vector3 arguments and receive Vector3 results (see remotes), and TweenService can tween Vector3 values (see services).
To put a world position on screen, pass its components to world_to_screen: world_to_screen(p.X, p.Y, p.Z).
Color3
Color3 is an immutable RGB color stored as three 32-bit floats, where each channel normally runs from 0 to 1.
There is no __eq or arithmetic: two equal colors are not ==. Compare R, G, B or ToRGB.
The draw.* functions do not take Color3
draw functions reject Color3; pass a { R, G, B, A } table instead. Drawing objects accept Color3.
Ranges and missing members
new takes 0-1, fromRGB takes 0-255. Neither clamps. fromHSV, fromHex, Lerp, ToHSV and ToHex don't exist.
health-color.lua
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player and player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
localfunction healthColor(fraction)
fraction = math.max(0, math.min(1, fraction))
return Color3.new(1 - fraction, fraction, 0)
endif humanoid and humanoid.MaxHealth > 0thenlocal color = healthColor(humanoid.Health / humanoid.MaxHealth)
local label = Drawing.new("Text")
label.Text = ("HP %d"):format(math.floor(humanoid.Health))
label.Position = Vector2.new(20, 20)
label.Size = 18
label.Outline = true
label.Color = color
print("health color", color:ToRGB())
end
Constructors
Color3.new([r], [g], [b])
Creates a color from channels in the 0-1 range. Missing or nil channels are 0, so Color3.new() is black.
Parameters:
r: number (optional, default 0) - Red, normally 0-1. Not clamped. Numeric strings are converted; any other non-nil value raises number expected.
g: number (optional, default 0) - Green, same rules as r.
b: number (optional, default 0) - Blue, same rules as r.
Returns:
Color3 - A new color. Channels are stored as 32-bit floats.
lua
local red = Color3.new(1, 0, 0)
local grey = Color3.new(0.5, 0.5, 0.5)
print(red, grey, Color3.new())
Color3.fromRGB([r], [g], [b])
Creates a color from 0-255 channels. Not clamped or rounded; missing channels are 0.
Parameters:
r: number (optional, default 0) - Red, normally 0-255.
g: number (optional, default 0) - Green, normally 0-255.
b: number (optional, default 0) - Blue, normally 0-255.
Returns:
Color3 - A new color whose channels are the inputs divided by 255.
lua
local orange = Color3.fromRGB(255, 128, 0)
print(orange.R, orange.G, orange.B)
Properties
Color3.R
The red channel as a Lua float, normally 0-1.
Read-only.
Returns:
number
Aliases:Color3.r
Color3.G
The green channel as a Lua float, normally 0-1.
Read-only.
Returns:
number
Aliases:Color3.g
Color3.B
The blue channel as a Lua float, normally 0-1.
Read-only.
Returns:
number
Aliases:Color3.b
Methods
Color3:ToRGB()
Returns the channels as 0-255 integers, rounded but not clamped.
Returns:
r: integer - Red, normally 0-255.
g: integer - Green, normally 0-255.
b: integer - Blue, normally 0-255.
The name is case-sensitive (c:torgb() fails), and the method is only reachable through a color: Color3.ToRGB on the global table is nil.
lua
local r, g, b = Color3.new(0.75, 0.25, 0):ToRGB()
print(r, g, b)
Operators
Expression
Result
Behavior
tostring(c)
string
The 0-1 channels formatted with %g (6 significant digits) and joined by a comma and space. Color3.fromRGB(10, 20, 30) prints as 0.0392157, 0.0784314, 0.117647.
typeof(c)
string
Color3. Plain type(c) returns userdata.
a == b
boolean
No __eq: true only when both sides are the same object.
+ - * /, unary -
error
Not defined. Build a new color from the channels instead.
Storage
Each Color3 is a full userdata holding three 32-bit floats (R, G, B, in that order), with the metatable named clover.Color3. That name is what appears in error messages, and typeof strips the clover. prefix.
Channels are rounded to single precision, so Color3.new(0.1).R reads back as 0.10000000149012.
Values are immutable. There is no __newindex, so c.R = 1 raises attempt to index a clover.Color3 value. Build a new color instead.
R, G and B are case-insensitive (c.r works). ToRGB is case-sensitive. Any other key reads as nil.
Where Color3 is used
Instance properties: Color (or its other name Color3) on parts, and Ambient, OutdoorAmbient and FogColor on Lighting. Assigning them accepts a Color3 or a plain table with R, G and B (or lowercase) fields, where a missing field counts as 0.
Drawing objects return a Color3 for Color. Setting Color accepts a Color3 or the same kind of table.
Remote calls can send Color3 arguments and receive Color3 results (see remotes), and TweenService can tween Color3 values (see services).
CFrame
A position plus a 3x3 rotation matrix, stored as twelve floats.
A minimal CFrame
Only CFrame.new, Position/p, X, Y, Z, LookVector, RightVector, UpVector and * exist. Everything else (Angles, lookAt, Inverse, Lerp, ToObjectSpace, ...) reads as nil. See the recipes for replacements.
Use CFrame.new, not CFrame(...)
CFrame(...) always errors. Use CFrame.new.
Case-sensitive members
Member names are case-sensitive: cf.x is nil.
facing-line.lua
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local line = Drawing.new("Line")
line.Color = Color3.fromRGB(80, 200, 255)
line.Thickness = 2
RunService.RenderStepped:Connect(function()
local player = Players.LocalPlayer
local character = player and player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
ifnot root then
line.Visible = falsereturnendlocal cf = root.CFrame
local tip = cf * Vector3.new(0, 0, -10)
local x1, y1, visible1 = world_to_screen(cf.X, cf.Y, cf.Z)
local x2, y2, visible2 = world_to_screen(tip.X, tip.Y, tip.Z)
line.From = Vector2.new(x1, y1)
line.To = Vector2.new(x2, y2)
line.Visible = visible1 and visible2
end)
cf * Vector3.new(0, 0, -10) is the point 10 studs in front of the part, because the local -Z axis is the LookVector.
Constructors
CFrame.new has three forms, chosen by its arguments. The first two always produce the identity rotation, so the result faces -Z.
CFrame.new([x], [y], [z])
Creates a frame at the given position with no rotation. CFrame.new() is the identity frame at the origin.
Parameters:
x: number (optional, default 0) - Position X. Numeric strings are converted; any other non-nil value raises number expected.
y: number (optional, default 0) - Position Y.
z: number (optional, default 0) - Position Z.
Returns:
CFrame - A new frame with the identity rotation.
With 4 to 11 arguments, everything after z is ignored. Rotation needs all 12.
CFrame.new(position)
Creates a frame at a Vector3 position with no rotation.
Parameters:
position: Vector3 - The position. Any further arguments are ignored.
Returns:
CFrame - A new frame with the identity rotation.
CFrame.new(position, lookAt) ignores lookAt. Use the look-at recipe.
lua
local spawnPoint = CFrame.new(Vector3.new(0, 5, 0))
print(spawnPoint, spawnPoint.LookVector)
All properties are read-only and computed on every access. In the descriptions, r00 through r22 are the matrix entries in the order of the 12-number constructor.
CFrame.Position
The position as a new Vector3.
Read-only.
Returns:
Vector3
Aliases:CFrame.p
CFrame.X
The X coordinate of the position, as a Lua float.
Read-only.
Returns:
number
CFrame.Y
The Y coordinate of the position, as a Lua float.
Read-only.
Returns:
number
CFrame.Z
The Z coordinate of the position, as a Lua float.
Read-only.
Returns:
number
CFrame.LookVector
The forward direction (-r02, -r12, -r22). Identity gives 0, 0, -1.
Read-only.
Returns:
Vector3
CFrame.RightVector
The right direction, the first column: r00, r10, r20. For the identity rotation it is 1, 0, 0.
Read-only.
Returns:
Vector3
CFrame.UpVector
The up direction, the second column: r01, r11, r21. For the identity rotation it is 0, 1, 0.
Read-only.
Returns:
Vector3
Operators
Expression
Result
Behavior
cf * v
Vector3
Transforms a point from the frame's local space to world space: the rotation times v, plus the position. CFrame.new(10, 0, 0) * Vector3.new(1, 2, 3) is 11, 2, 3.
a * b
CFrame
Composes two frames: b expressed in a's local space. The rotation is a's matrix times b's, and the position is a * b.Position.
cf * other
error
Any right-hand operand that is not a Vector3 or CFrame raises CFrame multiplication expects Vector3 or CFrame.
v * cf, n * cf
error
The CFrame must be on the left. A Vector3 on the left raises number expected.
a + b, a - b, -cf
error
Not defined. cf + v reaches Vector3's +, which rejects the CFrame. To move a frame without changing its rotation, use CFrame.new(delta) * cf.
a == b
boolean
No __eq: true only when both sides are the same object.
tostring(cf)
string
Only the position, formatted with %g and joined by a comma and space, such as 1, 2, 3. The rotation is not printed.
typeof(cf)
string
CFrame. Plain type(cf) returns userdata.
Recipes
These helpers rebuild missing Roblox functions from what does exist. They assume an orthonormal rotation, which is what parts report.
cframe-helpers.lua
localfunction lookAt(from, target)
local look = (target - from).Unit
local right = look:Cross(Vector3.yAxis).Unit
local up = right:Cross(look)
return CFrame.new(from.X, from.Y, from.Z, right.X, up.X, -look.X, right.Y, up.Y, -look.Y, right.Z, up.Z, -look.Z)
endlocalfunction pointToObjectSpace(cf, point)
local d = point - cf.Position
return Vector3.new(d:Dot(cf.RightVector), d:Dot(cf.UpVector), -d:Dot(cf.LookVector))
endlocalfunction translate(cf, delta)
return CFrame.new(delta) * cf
endlocal camera = lookAt(Vector3.new(0, 10, 20), Vector3.new(0, 5, 0))
print(camera.LookVector)
print(pointToObjectSpace(camera, camera * Vector3.new(1, 2, 3)))
print(translate(camera, Vector3.new(0, 10, 0)))
lookAt matches CFrame.lookAt(from, target) with the default up vector. It breaks down when target is straight above or below from, because the cross product with Vector3.yAxis is then zero.
pointToObjectSpace is the inverse of cf * v: it multiplies by the transposed rotation, which equals the inverse for an orthonormal matrix.
translate keeps the rotation of cf and adds delta to its position.
Storage
Each CFrame is a full userdata holding twelve 32-bit floats: the position X, Y, Z followed by the rotation matrix row by row (r00, r01, r02, r10, ... r22), with the metatable named clover.CFrame. That name is what appears in error messages, and typeof strips the clover. prefix.
Values are immutable. There is no __newindex, so cf.Position = v raises attempt to index a clover.CFrame value. Build a new frame instead.
Every constructor call and * allocates a new userdata, and two frames with the same contents are still different objects and different table keys.
Where CFrame is used
CFrame on parts (see Instance) is built from the part's position and rotation matrix; a part without physics data gives the identity frame at the origin. Assigning a CFrame writes both position and rotation; assigning a Vector3 or an X/Y/Z table writes only the position.
SetAttribute accepts a CFrame value (see attributes).
Remote calls can send CFrame arguments and receive CFrame results (see remotes).
TweenService cannot tween CFrame values: it raises Tween values must be finite numbers, Vector2, Vector3 or Color3 (see services).
http
Web requests and JSON.
Needs the unsafe API
Requests need Allow unsafe Lua API; otherwise they raise an error. JSONEncode, JSONDecode, GenerateGUID and UrlEncode always work.
Requests block, they do not yield
Requests block the whole VM until they finish. Don't call them every frame; fetch once and cache.
Limits
Timeouts: 10 s resolve, 10 s connect, 15 s send, 30 s per receive. Bodies are capped at 64 MiB. URLs must start with http:// or https://.
post-json.lua
local payload = HttpService:JSONEncode({player = "clover", score = 10})
local response = request({
Url = "https://httpbin.org/post",
Method = "POST",
Headers = {["Content-Type"] = "application/json"},
Body = payload,
})
if response.Success thenlocal data = HttpService:JSONDecode(response.Body)
print("server saw score", data.json.score)
else
warn("request failed", response.StatusCode, response.StatusMessage)
end
Functions
request(options)
Sends an HTTP request and returns a response table. Failed requests don't raise; check Success and StatusCode.
Requires the unsafe API.
Parameters:
options: table - Request description. See Request options for the keys.
Returns:
response: table - Always returned, also on failure. See Response table.
Aliases:http_request, HttpService:RequestAsync
HttpService:RequestAsync(options) calls the same code and returns the same table, so it does not raise for HTTP or network failures either.
Method is sent exactly as written, so use uppercase ("POST", not "post").
Body must already be a string. Tables are not encoded for you, so run them through HttpService:JSONEncode first.
Required. Absolute http:// or https:// URL. URL is read when Url is nil. A URL WinHTTP cannot parse gives StatusMessage = "invalid url".
Method
string
Optional, default "GET". Any verb ("POST", "PUT", "PATCH", "DELETE", ...) is passed through verbatim.
Headers
table
Optional. Request headers as name = value. Only pairs where both key and value are strings are sent, others are skipped silently.
Body
string
Optional. Raw request body, binary safe. Non-string values (other than numbers) are ignored and no body is sent.
Any other key (for example Cookies) is ignored.
Response table
Field
Type
Description
Success
boolean
true only when StatusCode is 200 to 299.
StatusCode
integer
HTTP status code. 0 when no response arrived (bad URL, DNS or connection failure, timeout) or the body exceeded 64 MiB.
StatusMessage
string
"OK" on success and "HTTP error" for any other HTTP status (not the server's reason phrase). On transport failure it is one of "invalid url", "WinHttpOpen failed", "WinHttpConnect failed", "WinHttpOpenRequest failed", "request failed" or "response too large".
Body
string
Raw response body, binary safe. Filled for error statuses too (a 404 page, an API error JSON). Empty string on transport failure.
Headers
table
Response headers as name = value. Names keep the server's casing. Values keep the space after the colon (usually " application/json"), and a repeated header such as Set-Cookie keeps only its last value.
read-header.lua
local response = request({Url = "https://example.com"})
local contentType = response.Headers["Content-Type"] or response.Headers["content-type"]
if contentType then
print(contentType:match("^%s*(.-)%s*$"))
end
HttpGet(url)
Performs a GET request and returns the response body. Unlike request, failures raise an error.
Requires the unsafe API.
Parameters:
url: string - Absolute http:// or https:// URL.
Returns:
body: string - Response body when the status is 200 to 299.
Errors: HttpGet failed: HTTP <code> for a non-2xx status, or HttpGet failed: <reason> when no response arrived (reason is a StatusMessage value from the response table).
Extra arguments are ignored, so game:HttpGet(url, true) works. Use request for headers.
game:HttpGet and game:HttpGetAsync only exist while game is set (you are in a game). The global HttpGet always works.
lua
local ok, source = pcall(HttpGet, "https://raw.githubusercontent.com/user/repo/main/script.lua")
ifnot ok then
warn(source)
returnendlocal chunk, problem = loadstring(source, "=remote")
if chunk then
chunk()
else
warn(problem)
end
HttpService
HttpService is a plain global table, also returned by game:GetService("HttpService"). Call its functions with a colon. HttpService:GetAsync is HttpGet and HttpService:RequestAsync is request.
HttpService:PostAsync(url, data, [contentType])
Sends a POST request with data as the body and a Content-Type header, and returns the response body. Implemented in Lua on top of request.
Requires the unsafe API.
Parameters:
url: string - Absolute http:// or https:// URL.
data: string - Request body. Encode tables with JSONEncode first.
contentType: string (optional, default "application/json") - Value of the Content-Type header.
Returns:
body: string - Response body when Success is true.
Raises HTTP <StatusCode> (prefixed with the caller's position) when the request does not succeed. Transport failures report HTTP 0.
lua
local body = HttpService:JSONEncode({content = "hello"})
local ok, result = pcall(HttpService.PostAsync, HttpService, "https://httpbin.org/post", body)
if ok then
print(result)
else
warn(result)
end
HttpService:JSONEncode(value)
Serializes a Lua value to a compact JSON string (no spaces or newlines). See JSON type mapping for how each type is written.
Parameters:
value: any - nil, boolean, number, string or a table of those.
Returns:
json: string - The encoded JSON text.
Raises an error for functions, userdata, threads, cycles, or nesting deeper than 64.
Parses JSON text into Lua values. Objects become tables with string keys, arrays become sequences starting at 1.
Parameters:
json: string - JSON text. Leading and trailing whitespace is allowed, any other trailing text is an error.
Returns:
value: any - The decoded value. JSON null at the top level returns nil.
Every JSON number decodes to a float: 3 becomes 3.0. Use math.tointeger(x) when you need an integer.
Raises JSONDecode: invalid JSON on any syntax error, trailing text or nesting deeper than 64 levels.
lua
local data = HttpService:JSONDecode('{"name":"clover","tags":["a","b"],"count":3,"extra":null}')
print(data.name, data.tags[2], data.count, math.type(data.count), data.extra)
print(math.tointeger(data.count))
HttpService:GenerateGUID([wrap])
Returns a random version-4 style GUID in lowercase hex, such as {3f2b9c1e-7a4d-4e0b-9c51-0d8e2a6f1b73}.
Parameters:
wrap: boolean (optional, default true) - Pass false to omit the curly braces.
Percent-encodes text for use in a URL. Letters, digits and -_.~ are kept, every other byte becomes %XX with uppercase hex (a space becomes %20).
Parameters:
text: string - Text to encode. Other values are passed through tostring first.
Returns:
encoded: string - The encoded text.
lua
local query = HttpService:UrlEncode("clover docs & more")
print("https://example.com/search?q=" .. query)
HttpService.HttpEnabled
Always true. Exists for scripts that check it before making requests. Whether requests actually work depends on the unsafe API toggle.
JSON type mapping
Encoding (Lua to JSON)
Lua value
JSON output
nil
null
boolean
true or false
number
Formatted with %.17g: 5 gives 5, 0.1 gives 0.10000000000000001, 1e20 gives 1e+20. Integers above 2^53 lose precision. NaN and infinity produce text that is not valid JSON.
string
Quoted. ", \, \b, \f, \n, \r, \t are escaped, other control bytes become \u00XX. Bytes 0x80 and up (UTF-8) and / are copied unchanged.
table with #t > 0
Array of t[1] to t[#t]. Holes inside that range become null. All other keys are dropped.
table with #t == 0
Object of its string keys. Non-string keys are dropped. An empty table gives {}. If the first key next returns is a positive integer, the whole table is written as [] and every key is lost: {[5] = "x"} always gives [], and a table that mixes string keys with positive integer keys but has no t[1] can come out as [] too.
function, userdata, thread
Error JSONEncode: unsupported value
Decoding (JSON to Lua)
JSON value
Lua value
object
Table with string keys. A repeated key keeps its last value.
array
Sequence table starting at index 1.
string
String. \uXXXX escapes become UTF-8. Surrogate pairs are not combined, so escaped emoji come out as invalid UTF-8.
number
Float, always (1 becomes 1.0). Parsed with C strtod, so it is a little more lenient than strict JSON.
true / false
boolean
null
nil. Object keys holding null are absent. Array entries become holes, so # can stop early: [1,null,3] decodes to a table where #t can be 1.
WebSocket
Connect to ws:// and wss:// servers.
Needs the unsafe API
Needs Allow unsafe Lua API. A VM reset closes every socket without firing OnClose.
Blocking and threading
connect and Send block until done. Incoming messages are delivered on the script thread, up to 64 per pass.
Limit
Value
Open sockets at once
8
URL length
2048 bytes
Outgoing message
8 MiB (8388608 bytes)
Incoming message
16 MiB (16777216 bytes), larger closes the socket with code 1009
Undelivered backlog, shared by all sockets
4096 events or 32 MiB of message data, more closes the socket with code 1008
Close reason
123 bytes
Handshake timeouts
5 s resolve, 5 s connect, 8 s send, 8 s receive
echo-socket.lua
local socket = WebSocket.connect("wss://echo.websocket.org")
socket.OnMessage:Connect(function(message, isBinary)
if isBinary then
print("binary message,", #message, "bytes")
else
print("text message:", message)
endend)
socket.OnClose:Connect(function(code, reason)
print("socket closed", code, reason)
end)
socket:Send(HttpService:JSONEncode({type = "hello", from = "clover"}))
socket:Send(crypt.random(16), true)
task.delay(10, function()
if socket:IsOpen() then
socket:Close()
endend)
Functions
WebSocket.connect(url)
Opens a WebSocket connection and returns a socket object once the handshake has completed. Raises WebSocket connect failed: <reason> when it cannot connect.
Requires the unsafe API.
Parameters:
url: string - A ws:// or wss:// URL. See URL rules.
Returns:
socket: table - A new table with Send, Close, IsOpen, OnMessage and OnClose.
Only the lowercase name connect exists. WebSocket:connect(url) also works.
The handshake sends no extra headers: custom headers, cookies and subprotocols are not supported.
wss:// uses TLS and the server certificate must verify.
Common reasons: too many open WebSockets (the limit is 8), close one first, the url must start with ws:// or wss://, the server refused the WebSocket upgrade (HTTP <status>), the host name could not be resolved (...), could not connect to the server (...), the request timed out (...), the TLS certificate could not be verified (...).
lua
local ok, socket = pcall(WebSocket.connect, "ws://127.0.0.1:8080/feed?room=1")
ifnot ok then
warn(socket)
returnend
print("connected", socket:IsOpen())
URL rules
Part
Rule
Scheme
ws:// or wss://, any letter case. Anything else fails with the url must start with ws:// or wss://.
Host
Letters, digits, ., - and _, or an IPv6 address in brackets such as ws://[::1]:8080.
Port
Optional, 1 to 65535. Defaults to 80 for ws:// and 443 for wss://.
Path and query
Sent as written, / when empty. A #fragment is dropped.
Not allowed
user:pass@ credentials, spaces, control characters, non-ASCII bytes (percent-encode them) and URLs over 2048 bytes.
Methods
socket:Send(message, [binary])
Sends one message. Text messages must be valid UTF-8. Raises WebSocket send failed: <reason> on failure.
Parameters:
message: string - The payload, up to 8 MiB. Numbers are converted to strings.
binary: boolean (optional, default false) - Any truthy value sends a binary frame instead of a text frame.
Reasons: the socket is closed, the message is too large (the limit is 8 MB), text messages must be valid UTF-8, pass true as the second argument to send binary data, or a WinHTTP send error.
Raw bytes such as crypt.random(16) are usually not valid UTF-8, so send them with binary set to true.
lua
local socket = WebSocket.connect("wss://echo.websocket.org")
socket:Send("hello")
socket:Send(string.pack("<I4f", 7, 1.5), true)
socket:Close()
Closes the connection. OnClose fires on a later pass.
Calling it again, or after OnClose has fired, does nothing.
lua
local socket = WebSocket.connect("wss://echo.websocket.org")
socket:Close()
print(socket:IsOpen())
socket:IsOpen()
Reports whether the socket is connected and usable.
Returns:
open: boolean - false once you call Close, the server closes, or the connection drops. It can turn false before OnClose has fired.
lua
local socket = WebSocket.connect("wss://echo.websocket.org")
if socket:IsOpen() then
socket:Send("still here")
end
Events
OnMessage and OnClose are signals. A handler that errors is disconnected.
socket.OnMessage
Fires for every complete message received. Fragmented messages are reassembled first, so a handler always gets the whole message.
Parameters:
message: string - The message bytes.
isBinary: boolean - true for a binary message, false for text.
lua
local socket = WebSocket.connect("wss://echo.websocket.org")
socket.OnMessage:Connect(function(message, isBinary)
if isBinary thenreturnendlocal ok, data = pcall(HttpService.JSONDecode, HttpService, message)
if ok and type(data) == "table"then
print("event", data.type)
endend)
socket.OnClose
Fires once when the connection ends. Afterwards the socket is released: IsOpen returns false and Send raises the socket is closed.
Hashes data with Windows CNG and returns the digest as lowercase hex.
Parameters:
data: string - Bytes to hash, up to 256 MiB. Numbers are converted to strings.
algorithm: string (optional, default "sha256") - "md5", "sha1", "sha256", "sha384" or "sha512", any letter case. Spellings like "sha-256" are rejected.
Returns:
digest: string - Hex digest: 32 characters for md5, 40 for sha1, 64 for sha256, 96 for sha384, 128 for sha512.
Raises crypt.hash: unsupported algorithm (md5, sha1, sha256, sha384, sha512) or input too large.
Returns count bytes from the system cryptographic random generator (BCryptGenRandom).
Parameters:
count: integer (optional, default 16) - Number of bytes, 0 to 1048576 (1 MiB). 0 returns an empty string.
Returns:
bytes: string - Raw random bytes of length count.
Raises crypt.random: count must be between 0 and 1048576 for a count outside that range. A non-integral number such as 1.5 raises a standard argument error.
lua
local nonce = crypt.random(12)
print(#nonce, base64encode(nonce))
crypt.base64encode(data)
Encodes bytes as standard base64 (alphabet A-Z a-z 0-9 + /) with = padding and no line breaks.
Parameters:
data: string - Bytes to encode. Numbers are converted to strings.
local encoded = crypt.base64.encode("clover")
print(base64.decode(encoded))
print(crypt.base64.encode == base64encode)
bit32
The bit32 library: bitwise operations on 32-bit unsigned integers, in the style of Lua 5.2 and Luau.
How numbers are converted
Arguments are truncated toward zero and reduced to 32 bits, so -1 becomes 4294967295. Results are integers from 0 to 4294967295.
Shift counts wrap at 32
Shift counts are masked with & 31: bit32.lshift(1, 32) returns 1, and a negative count does not reverse direction. Use rshift for right shifts.
Lua 5.4's own bitwise operators (&, |, ~, <<, >>) work on 64-bit integers and are available too. Use bit32 when you want 32-bit wraparound. See Standard Library.
Raw reads and writes of the attached Roblox process (RobloxPlayerBeta.exe) at absolute addresses.
No safety net
Always available, even without the unsafe API. clover.memory_write writes into live Roblox; a wrong write can crash it. Memory layouts change with Roblox updates.
Failures are silent
Bad addresses never raise: they read as 0, false or "", and failed writes are ignored.
clover table only
Call them through clover, for example clover.read_int(...). Each call is a synchronous cross-process read.
pe-header.lua
local base = clover.getbase()
if base == 0then
warn("clover is not attached to Roblox")
returnendlocal magic = clover.memory_read("short", base)
local peHeader = clover.read_int(base + 0x3C)
local signature = clover.memory_read("uint", base + peHeader)
local sections = clover.memory_read("short", base + peHeader + 6)
print(string.format("base 0x%X", base))
print("MZ header:", magic == 0x5A4D, "PE signature:", signature == 0x4550)
print("sections:", sections)
Address arguments
Addresses are absolute 64-bit integers like 0x7FF6A1230000. They aren't checked before reading.
Places to get addresses from:
clover.getbase(): the base of the RobloxPlayerBeta.exe module. Add module-relative addresses to it.
Instance.Address on any Instance: the address of the Roblox object behind it. See Instance.
The address and humanoid fields of the entries returned by clover.players() and clover.local_player(). See players.
clover.memory_read and clover.memory_write take one of these exact, lowercase type names as their first argument. Any other name raises memory_read: unknown type '<name>' (or memory_write: ...).
Type
Size
memory_read returns
memory_write expects
"byte"
1 byte, unsigned
integer 0 to 255
integer; only the low 8 bits are stored
"bool"
1 byte
true if the byte is not 0
any value; truthy writes 1, nil or false writes 0
"short"
2 bytes, signed
integer -32768 to 32767
integer; only the low 16 bits are stored
"int"
4 bytes, signed
integer
integer; only the low 32 bits are stored
"uint"
4 bytes, unsigned
integer 0 to 4294967295
integer; only the low 32 bits are stored
"int64"
8 bytes, signed
integer
integer
"uintptr_t"
8 bytes
integer (pointer value)
integer
"float"
4 bytes
number (single precision)
number
"double"
8 bytes
number
number
"string"
variable
string in the std::string layout, up to 255 bytes; see clover.read_string
string; its raw bytes are copied to address, nothing else
Integer types accept floats with an exact integer value (5.0), but a fractional value such as 5.5 raises number has no integer representation.
Functions
clover.memory_read(type, address)
Reads one value of the given type at address and converts it to a Lua value.
value: integer | number | boolean | string - The value read. When the address is 0 or cannot be read you get the zero value of the type: 0, 0.0, false or "".
lua
local base = clover.getbase()
local peHeader = clover.memory_read("int", base + 0x3C)
local sections = clover.memory_read("short", base + peHeader + 6)
print("PE header at +" .. peHeader .. ", sections: " .. sections)
clover.memory_write(type, address, value)
Writes value to address as the given type. Returns nothing and gives no indication of whether the write succeeded.
value: any - An integer, number, any value for "bool", or a string, matching type.
"string" copies raw bytes only, with no terminator or length update.
Not gated by the unsafe API toggle.
lua
localfunction patchFloat(address, value)
local before = clover.memory_read("float", address)
clover.memory_write("float", address, value)
return before, clover.memory_read("float", address)
end
clover.read_int(address)
Reads a signed 32-bit integer. Same as clover.memory_read("int", address).
Parameters:
address: integer - Absolute address to read.
Returns:
integer - The value, or 0 if the read failed.
lua
local base = clover.getbase()
print("e_lfanew:", clover.read_int(base + 0x3C))
clover.read_int64(address)
Reads a signed 64-bit integer. Same as clover.memory_read("int64", address).
Parameters:
address: integer - Absolute address to read.
Returns:
integer - The value, or 0 if the read failed.
clover.read_float(address)
Reads a 32-bit float. Same as clover.memory_read("float", address).
Parameters:
address: integer - Absolute address to read.
Returns:
number - The value, or 0.0 if the read failed.
clover.read_pointer(address)
Reads an 8-byte pointer. Same as clover.memory_read("uintptr_t", address).
Parameters:
address: integer - Absolute address to read.
Returns:
integer - The pointer value, or 0 if the read failed.
lua
localfunction follow(address, steps)
for _, step in ipairs(steps) do
address = clover.read_pointer(address + step)
if address == 0thenreturnnilendendreturn address
end
clover.read_string(address)
Reads an MSVC std::string at address.
Parameters:
address: integer - Address of the std::string object (not of its characters).
Returns:
string - The bytes read. Empty when address is outside 0x10000 to 0x7FFFFFFEFFFF, when the length is 0, negative or over 255, or when the data pointer is outside that range.
Strings longer than 255 bytes cannot be read this way and come back empty.
The bytes are returned as-is and may contain zero bytes. If the length is readable but the characters are not, the result is that many zero bytes.
Same as clover.memory_read("string", address).
lua
localfunction readName(stringAddress)
local text = clover.read_string(stringAddress)
if text == ""thenreturnnilendreturn text
end
clover.getbase()
Returns the base address of the RobloxPlayerBeta.exe module in the attached process.
Returns:
integer - The module base, or 0 when no Roblox client is attached.
Aliases:clover.module_base
lua
local base = clover.getbase()
if base ~= 0then
print(string.format("RobloxPlayerBeta.exe at 0x%X", base))
end
Remotes
Fire remotes, invoke RemoteFunctions and trigger signals and ProximityPrompts.
Locked to one Roblox build
Only works on the supported Roblox build; otherwise raises <name>: unsupported Roblox build. No unsafe API needed.
Calls block the whole VM
Each call runs inside Roblox and blocks the VM, from about a second up to 10 s or more for invokeserver. Never call them every frame.
Errors are raised
Failures raise an error starting with the function name. Use pcall (see Errors).
buy-item.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buy = ReplicatedStorage:WaitForChild("BuyItem", 5)
local price = ReplicatedStorage:FindFirstChild("GetPrice")
if price thenlocal ok, cost = pcall(invokeserver, price, "Sword")
if ok then
print("Sword costs", cost)
else
warn(cost)
endendif buy thenlocal ok, err = pcall(function()
buy:FireServer("Sword", 1)
end)
ifnot ok then
warn("FireServer failed:", err)
endend
Argument types
fireserver and invokeserver accept at most 16 arguments after the remote. Each one is converted to a Roblox value by its Lua type:
Lua value
Sent as
Notes
boolean
bool
number
double
Integers and floats are both sent as a 64-bit float.
string
string
Strings up to 15 bytes are stored inline. Longer strings share a 4 KB argument block with the other arguments: one long string can be at most 3935 bytes, and the space shrinks as more arguments are passed.
Sent as a shared reference to the instance. An instance that is not held by a live shared reference in Roblox raises instance is not shared-owned.
anything else
rejected
nil, tables, functions, threads and other userdata raise unsupported argument type. A table that looks like a Vector3 is not accepted; pass a real Vector3.
If Roblox has not used one of the argument types yet in this session, the call fails with argument type not ready in this session. For Color3 and Instance arguments the engine first tries to initialise the type with an extra engine call.
Values returned by invokeserver
invokeserver returns every value in the server's reply, in order, as separate return values. A reply with more than 32 values, or a string longer than 64 KB, raises InvokeServer: could not read the reply.
Reply value
Lua value
bool
boolean
number
number, always a float: 5 comes back as 5.0. Use math.tointeger when you need an integer.
Fires a RemoteEvent to the server. Doesn't wait for the server. See Argument types.
Parameters:
remote: Instance - A RemoteEvent. Other classes, including UnreliableRemoteEvent, raise an error.
...: any (optional) - Up to 16 values of the supported types.
Aliases:Instance:FireServer
remote:FireServer(...) is the same function. Non-RemoteEvents raise an error.
FireServer: engine call did not run means no Roblox job picked up the call within about 1 s.
lua
local remote = game:GetService("ReplicatedStorage"):FindFirstChild("Chat")
if remote then
fireserver(remote, "hello", 42, Vector3.new(0, 10, 0), true)
end
invokeserver(remote, ...)
Invokes a RemoteFunction and waits up to about 10 s for the reply, then returns it.
Parameters:
remote: Instance - A RemoteFunction. Any other class raises InvokeServer: not a RemoteFunction.
...: any (optional) - Up to 16 values of the supported types.
Returns:
values: ...any - One Lua value per value in the server's reply (see Values returned by invokeserver). Nothing when the server returns nothing.
Aliases:Instance:InvokeServer
Raises an error after 10 seconds without a reply.
One call at a time. If 64 calls are still waiting for late replies, new calls raise an error.
When Roblox reports an error for the invocation, it raises InvokeServer: server error: followed by Roblox's error text, truncated to 223 bytes.
Argument errors use the InvokeServer: prefix, for example InvokeServer: unsupported argument type.
lua
local stats = game:GetService("ReplicatedStorage"):FindFirstChild("GetStats")
if stats thenlocal ok, coins, level = pcall(invokeserver, stats, "coins")
if ok then
print("coins", math.tointeger(coins) or coins, "level", level)
else
warn(coins)
endend
firesignal(instance, signal, [argument])
Calls the game's own handlers for a supported signal on instance. See the supported signals.
Parameters:
instance: Instance - The Humanoid, ClickDetector or ProximityPrompt that owns the signal.
signal: string - Signal name, case-sensitive: Jumping, Died, MouseClick, RightMouseClick, MouseHoverEnter, MouseHoverLeave or Triggered.
argument: Player | boolean (optional) - The Player for ClickDetector and ProximityPrompt signals, or a boolean for Humanoid.Jumping (default true).
Raises an error if a connection uses a callback type the engine doesn't support.
Raises an error when the signal has no connections.
The argument must be a Player Instance for ClickDetector and ProximityPrompt signals.
This fires the game's handlers inside Roblox. It does not fire connections your script made on the clover signal objects.
lua
local player = game:GetService("Players").LocalPlayer
local button = workspace:FindFirstChild("Button")
local detector = button and button:FindFirstChildOfClass("ClickDetector")
if detector then
firesignal(detector, "MouseClick", player)
end
Signals firesignal can fire
Class
Signal
Third argument
Humanoid
Jumping
boolean, defaults to true
Humanoid
Died
not needed
ClickDetector
MouseClick
the Player (required)
ClickDetector
RightMouseClick
the Player (required)
ClickDetector
MouseHoverEnter
the Player (required)
ClickDetector
MouseHoverLeave
the Player (required)
ProximityPrompt
Triggered
the Player (required)
Any other class and signal pair raises firesignal: signal not supported. If the signal's location in memory does not match the current build it raises firesignal: signal descriptor does not match this build.
fireproximityprompt(prompt)
Holds and releases a ProximityPrompt as if the player used it.
Parameters:
prompt: Instance - A ProximityPrompt. Anything else raises fireproximityprompt: argument must be a ProximityPrompt.
HoldDuration is read from the prompt. A value of 30 s or more, or 0 or less, is treated as 0, so the wait is then only 200 ms.
Extra arguments that some executors accept (an amount or a skip-hold flag) are ignored.
It does not move your character or change the prompt's properties.
It does not fire your script's own prompt.Triggered connections; use prompt:Fire for those.
Raises fireproximityprompt: InputHoldBegin did not run or fireproximityprompt: InputHoldEnd did not run when the matching engine call failed.
lua
local door = workspace:FindFirstChild("Door")
local prompt = door and door:FindFirstChildOfClass("ProximityPrompt")
if prompt then
fireproximityprompt(prompt)
end
Methods
Instance:Fire(...)
Fires your own connections on a BindableEvent's Event or a ProximityPrompt's Triggered. Nothing is sent to Roblox.
Parameters:
...: any (optional) - Any Lua values, passed unchanged to the handlers.
Any other class raises Fire: unsupported instance.
Handlers run before Fire returns. A handler that raises an error is logged and disconnected.
local bindable = game:GetService("ReplicatedStorage"):FindFirstChildOfClass("BindableEvent")
if bindable then
bindable.Event:Connect(function(message, count)
print("got", message, count)
end)
bindable:Fire("hello", 3)
end
OnClientEvent is local to the VM
OnClientEvent, Event and Triggered only fire when a script calls :Fire(...), never from real game traffic.
Errors
Message
Raised by
Meaning
unsupported Roblox build
all four
The running Roblox build isn't supported yet.
not a RemoteEvent / not a RemoteFunction
fireserver / invokeserver
The first argument has the wrong class.
too many arguments
fireserver, invokeserver
More than 16 arguments after the remote.
unsupported argument type
fireserver, invokeserver
An argument is not a boolean, number, string, Vector2, Vector3, Color3, CFrame or Instance.
bad CFrame argument (or Vector3, Color3, Vector2)
fireserver, invokeserver
A value of that type could not be read.
instance is not shared-owned
fireserver, invokeserver
The engine cannot take a reference on an Instance argument.
argument type not ready in this session
fireserver, invokeserver
Roblox has not initialised that value type yet.
allocation failed
fireserver, invokeserver, firesignal
Memory could not be allocated in the Roblox process.
could not write argument block
fireserver, invokeserver
The long strings do not fit in the 4 KB argument block, or the block could not be written.
could not install the reply handlers
invokeserver
The first-use setup of the reply handlers failed.
could not write the callbacks
invokeserver
The reply callbacks could not be written to Roblox memory.
too many calls are still waiting for a reply
invokeserver
All 64 reply slots are held by unanswered calls.
no reply from the server within 10 seconds
invokeserver
Timeout.
server error: ...
invokeserver
Roblox reported an error for the invocation.
could not read the reply
invokeserver
The reply had more than 32 values, a string over 64 KB, or could not be read.
engine call did not run
fireserver, invokeserver, firesignal
No Roblox job picked up the call within about 1 s, or the call could not be set up.
first argument must be an Instance
firesignal
The first argument is not an Instance.
signal not supported
firesignal
The class and signal pair is not in the supported list, or Humanoid.Jumping has no connections.
signal descriptor does not match this build
firesignal
The signal could not be located on this build.
signal has no connections
firesignal
Nothing is connected to the signal.
signal has no supported connections
firesignal
A connection uses a callback the engine cannot call.
third argument must be the Player / third argument must be a Player
firesignal
The Player argument is missing, not an Instance, or not a usable Player.
argument must be a ProximityPrompt
fireproximityprompt
The argument is not a ProximityPrompt.
InputHoldBegin did not run / InputHoldEnd did not run
fireproximityprompt
The engine call failed.
Fire: unsupported instance
Instance:Fire
The instance is not a BindableEvent or ProximityPrompt.
Messages from fireserver and Instance:FireServer start with FireServer: , from invokeserver and Instance:InvokeServer with InvokeServer: , and from the other two with firesignal: and fireproximityprompt: .
Scripts
List game scripts, read their bytecode and decompile it.
Read from client memory
These read Roblox memory and never run code in the game, so getsenv always returns an empty table. No unsafe API needed.
decompile always returns a string
They don't raise on bad input: getscriptbytecode returns "" and decompile may return a disassembly or failure report. Check the result.
Synchronous
Decompiling blocks the VM until done. Don't call it every frame.
dump-scripts.lua
local me = game:GetService("Players").LocalPlayer
ifnot me then
warn("not in a game yet")
returnendifnot isfolder("dump") then
makefolder("dump")
endlocal scripts = getscripts(me)
print(#scripts .. " scripts under " .. me.Name)
for index, script in ipairs(scripts) dolocal bytecode = getscriptbytecode(script)
if #bytecode > 0thenlocal source = decompile(bytecode)
ifnot source:find("^Decompile failed") thenlocal name = (script.Name:gsub("[^%w_]", "_"))
writefile("dump/" .. index .. "_" .. name .. ".lua", source)
endendend
Functions
getscripts([root])
Returns every LocalScript, Script and ModuleScript under root, including root itself.
Parameters:
root: Instance (optional, default game) - Instance to start from. Omitted or nil uses the DataModel. Any other non-Instance value (including false) raises an argument error.
Returns:
scripts: table - Array of script Instances in the order they were found. Empty when the DataModel cannot be read (no game loaded).
The walk stops after 8000 scripts or after visiting 120000 instances, whichever comes first, so very large games give a partial list.
An instance with more than 10000 children is treated as having none, so nothing below it is visited.
lua
for _, script in ipairs(getscripts(game:GetService("ReplicatedStorage"))) do
print(script.ClassName, script:GetFullName())
end
getscriptbytecode(script)
Returns a script's compiled bytecode as a binary string, or "" if it can't be read.
Parameters:
script: Instance - A LocalScript, Script or ModuleScript.
Returns:
bytecode: string - The raw bytes exactly as the client stores them. "" when script is not an Instance, is not one of the three script classes, has no bytecode on the client, cannot be read, or is larger than 16 MiB.
May be Roblox's compressed RSB1 format. decompile handles it.
Passing a non-Instance value never raises; it returns "".
lua
local scripts = getscripts()
for i = 1, math.min(#scripts, 10) dolocal bytes = getscriptbytecode(scripts[i])
if #bytes > 0then
print(scripts[i]:GetFullName(), #bytes .. " bytes", string.format("first byte 0x%02X", bytes:byte(1)))
endend
decompile(source)
Turns bytecode into Luau source. Falls back to a disassembly, then a failure report. See Output forms.
Parameters:
source: Instance | string - A script Instance or a bytecode string.
Returns:
text: string - Decompiled source, a disassembly listing, or a message. Never nil.
Returns -- decompile: no bytecode when nothing can be read.
The MCP server uses this function for its script reading tool.
lua
local target = game:GetService("ReplicatedStorage"):FindFirstChild("Shared")
if target thenlocal text = decompile(target)
if text:find("^Decompile failed") then
warn(text)
else
print(text)
endend
getsenv(script)
Compatibility stub that always returns an empty table. Use getgc or findgc to inspect game state.
Parameters:
script: Instance (optional) - Ignored.
Returns:
env: table - A fresh empty table on every call.
lua
local env = getsenv(game)
print(next(env) == nil)
Output forms
decompile reports problems in its return value rather than by raising. The start of the text tells you which path produced it:
Text starts with
Meaning
Luau source
The decompiler succeeded.
; Luau bytecode
The decompiler failed but the disassembler parsed the bytecode. The listing gives the bytecode version, the string and function counts, and for every function its constants and its instructions with their operands.
Decompile failed.
Neither path could handle the bytes. The text includes the decompiler's message and the disassembler's message, for example when the blob is empty or is still encrypted.
-- decompile: no bytecode
The argument was not a string and no bytecode could be read from it.
Garbage Collector
Scan the game's Luau heap from outside Roblox, inspect its tables and closures, and tune its garbage collector.
This is the game's VM, not yours
These read the game's Luau heap through process memory, not clover's own state. Results are proxies that point at game addresses. See Proxy objects.
Only on the supported client build
Only works on the supported Roblox build. Otherwise most functions return nil, "no game lua state".
Writes go straight into game memory
Writing to proxies, debug.setupvalue and setgc change the game's heap directly with no safety checks. A wrong write corrupts the game.
Scans are synchronous and proxies are never freed
Scans block the VM until done, and getgc(true) can create 120000 proxies. Don't scan every frame.
gc-ammo.lua
local info, problem = getgc_info()
ifnot info then
warn("game VM not available: " .. tostring(problem))
returnend
print(string.format("game heap: %d KB, collector running: %s", info.kb, tostring(info.isrunning)))
local iter, configs = findgc("MaxAmmo")
if iter thenfor _, config in ipairs(configs) dolocal current = config.MaxAmmo
if type(current) == "number"then
print("MaxAmmo was " .. current)
config.MaxAmmo = 999endendend
Proxy objects
Results are proxies: empty tables that store a game address. Each access re-checks the address. Every read makes a new proxy, so two proxies for the same object are never ==.
Table proxies
Operation
Behaviour
t.key, t["key"]
Scans the game table's hash part for the string key. Returns nil, a boolean, a number, a string (strings over 65536 bytes read as nil), a table proxy or a function proxy. Any other value type (userdata, vector, thread, buffer) reads as nil.
t[1]
A number key is converted to a string first, so this looks up the string key "1". The array part of a game table is never read.
Nested tables
A table value whose hash part is empty (a pure array or an empty table) reads as nil, as does one with more than 2^18 hash slots or more than 1000000 array slots.
t.key = value
Only overwrites a key that already exists, and only with a number (numeric strings count as numbers) or a boolean. It replaces the old value whatever its type was. Other values, missing keys and nil are ignored without an error.
See nothing, because the proxy itself is empty. Use getrawkeys.
Write slot cache
Writes cache the slot they find. If the game resizes the table in between, a later write can hit the wrong slot.
Function proxies
A function proxy can't be called or indexed. Use it with the debug additions. debug.getinfo(fn).addr gives the real game address.
If a closure changes after the scan, the debug functions treat its proxy as stale.
Functions
getgc([include_tables])
With true, returns an iterator over the game's tables. Without it, returns the same info table as getgc_info. Functions are never included; use getclosures.
Parameters:
include_tables: any (optional, default nil) - Any truthy value selects the table scan.
Returns:
iter: function - Iterator that yields index, proxy pairs.
list: table - The array of table proxies the iterator walks.
start: number - Always 0.
Returns nil, "no game lua state" when no game is loaded; check before looping.
local list = getgc(true) stores the iterator function. Take the second value when you want the array.
Skips empty and huge tables and stops at 120000 tables.
lua
local iter, list = getgc(true)
ifnot iter then
warn("getgc failed: " .. tostring(list))
returnend
print(#list .. " game tables found")
findgc(key, [value])
Returns an iterator over game tables that have the string key key (and, if given, the string value). With keynil, matches tables holding value under any key.
Parameters:
key: string | nil - String key to look for. Numbers are accepted and compared as their string form. nil selects the value-only search.
value: string (optional) - String the value must equal. Required when key is nil.
Returns:
iter: function - Iterator that yields index, proxy pairs.
list: table - The array of matching table proxies.
start: number - Always 0.
Returns nil, "no game lua state" when no game VM is found.
Raises findgc(key|keys) or findgc(key,value) or findgc(nil,value) when the arguments fit none of the forms, for example findgc() or findgc(nil).
Stops after 8000 matching tables.
lua
for _, weapon in findgc("Damage") do
print(weapon.Name, weapon.Damage)
endfor _, entry in findgc(nil, "Sniper") do
print("table holding Sniper", getrawkeys(entry).Name)
end
findgc(keys)
Batch form: one pass over the heap for several keys at once. Returns a plain table that maps every accepted key to an array of table proxies whose hash part has that string key.
Parameters:
keys: table - Array of keys (numbers are converted to strings). Empty strings and duplicates are skipped, and only the first 128 distinct keys are used.
Returns:
results: table - results[key] is an array of table proxies for every accepted key, possibly empty.
At most 400 tables are collected per key. The scan ends early once every key has 400.
A table that has several of the keys appears in each of their lists, as separate proxies.
When no game VM is found, it still returns results with empty lists, not nil and a message.
lua
local found = findgc({"Ammo", "MaxAmmo", "ReloadTime"})
for key, tables in pairs(found) do
print(key, #tables)
end
closures: table - Array of function proxies, at most 60000 across all game VMs.
Returns nil, "no game lua state" when no game VM is found.
Closures without upvalues, or with more than 62, are never listed.
Each proxy records its upvalue count at scan time, which debug.getupvalue, debug.getupvalues and debug.setupvalue use to detect a stale proxy.
lua
local closures, problem = getclosures()
ifnot closures then
warn(problem)
returnendlocal lua_count = 0for _, fn in ipairs(closures) dolocal info = debug.getinfo(fn)
if info andnot info.is_c then
lua_count = lua_count + 1endend
print(#closures .. " closures, " .. lua_count .. " Lua closures")
getrawkeys(proxy)
Takes a snapshot of a table proxy. Returns a new ordinary Lua table that maps every string key in the game table's hash part to its value, converted the same way as a proxy read.
Parameters:
proxy: table - A table proxy. Anything else, including a function proxy, gives an empty table.
Returns:
entries: table - Plain table of key = value. Changing it does not change the game.
Keys that are empty, longer than 128 bytes or not strings are skipped, and the array part is not read.
Entries whose value is nil or a type proxies cannot represent (userdata, vector, thread, buffer, or a table with an empty hash part) are left out.
lua
for _, config in findgc("MaxAmmo") dofor key, value in pairs(getrawkeys(config)) do
print(key, value)
endbreakend
getgc_info()
Reads collector statistics from the largest game VM (the one with the most allocated bytes). getgc() with no argument returns the same table.
Returns nil, "no game lua state" when no game VM is found.
List counts only cover the collector's gray, grayagain and weak lists, up to 80000 objects each.
lua
local info = getgc_info()
if info then
print("heap KB", info.kb, "state", info.gcstate, "pages", info.pages)
for name, count in pairs(info.by_type) do
print(name, count)
endend
getgc_info fields
Field
Type
Description
kb
number
Total bytes allocated by the game VM, divided by 1024 and rounded down. Pushed as a float.
gcstate
integer
Collector phase byte from the game's global state. A VM is only accepted when this is between 0 and 4.
isrunning
boolean
false while the GC threshold is -1 (after setgc("stop")), otherwise true.
pause
integer
The game's gcpause setting.
stepmul
integer
The game's gcstepmul setting.
pages
integer
Heap pages on both page lists that passed the walker's checks (block sizes 40 to 1024 bytes, at most 4096 pages per list).
gray
integer
Objects on the gray list.
grayagain
integer
Objects on the grayagain list.
weak
integer
Objects on the weak list.
total
integer
gray + grayagain + weak.
L
string
Address of the game's lua_State, as hex text like 0x1A2B3C40.
G
string
Address of the game's global state, as hex text.
by_type
table
Counts of the listed objects keyed by type name: nil, bool, lightuserdata, number, vector, string, table, function, userdata, thread, buffer or other. Only types with a non-zero count are present.
samples
table
Array of up to 24 listed objects, each {tt = integer type tag, name = type name, addr = hex string}.
setgc(option, [value])
Reads or changes the collector settings of the largest game VM by reading and writing its global state directly. See setgc options for what each option returns.
Parameters:
option: string - One of the option names below. A missing option raises an argument error.
value: integer (optional) - Integer value for pause/setpause or stepmul/setstepmul.
Returns:
result: any - Depends on the option. Failures return nil and a message.
Returns nil, "no game lua state" when no game VM is found, before the option name is matched.
lua
local before = setgc("pause", 200)
print("gcpause was", before, "now", setgc("pause"))
if setgc("isrunning") then
print("game heap KB", setgc("count"))
end
setgc options
Option
Effect
Returns
"stop"
Writes -1 into the game's GC threshold.
true
"restart"
Sets the GC threshold to the current total bytes.
true
"count"
Reads total bytes allocated.
number of KB (float)
"isrunning"
Reads the GC threshold.
false while it is -1, else true
"pause", "setpause"
With value, writes it to gcpause, clamped to 0..1000.
previous gcpause (integer)
"stepmul", "setstepmul"
With value, writes it to gcstepmul, clamped to 0..10000.
previous gcstepmul (integer)
"collect", "step"
Not supported, since a collection step must run inside the game.
nil, "collect/step need ingame call"
anything else
Nothing.
nil, "unknown setgc option"
gcprobe()
Diagnostic dump of every game VM found: how many tables the scan sees, how many string keys they hold, and a sample of key names. It runs the full table scan once per VM, so it is slow.
Returns nil, "no game lua state" when no game VM is found.
lua
local probe = gcprobe()
if probe thenfor _, vm in ipairs(probe.vms) do
print("vm", vm.index, vm.kb .. " KB", vm.hash_tables .. " tables", vm.str_keys .. " string keys")
print(table.concat(vm.sample_keys, ", "))
endend
gcprobe fields
Field
Type
Description
vm_count
integer
Number of game VMs found (1 or 2).
vms
table
Array with one entry per VM, described below.
vms[i].index
integer
Which of the two ScriptContext VM slots it came from (0 or 1).
vms[i].kb
integer
Total bytes allocated by that VM, divided by 1024.
vms[i].hash_tables
integer
Tables found by the same scan getgc(true) uses (at most 120000).
vms[i].str_keys
integer
Hash slots with a string key across those tables.
vms[i].G
string
Address of the VM's global state, as hex text.
vms[i].L
string
Address of the VM's lua_State, as hex text.
vms[i].sample_keys
table
Up to 15 non-empty string keys (at most 96 bytes each) in the order they were met.
ammo_vm
integer
Always 0.
debug additions
clover adds four functions to debug for game function proxies. With the unsafe API on, they replace the standard getinfo, getupvalue and setupvalue.
All four raise expected a game function from getgc or getclosures when the first argument is not a function proxy, and closure layout is unavailable for this Roblox build when the closure layout fails its sanity checks.
debug.getinfo(fn)
Describes a game closure. Takes no what string and returns its own set of fields.
Parameters:
fn: table - A function proxy.
Returns:
info: table - Fields is_c, what, nups, addr, env and proto; see Function info fields. nil when the address no longer holds a function.
lua
for _, fn in ipairs(getclosures() or {}) dolocal info = debug.getinfo(fn)
if info and info.what == "Lua"and info.nups > 3then
print(info.addr, info.proto, info.nups)
endend
Function info fields
Field
Type
Description
is_c
boolean
true for a C closure.
what
string
"C" or "Lua".
nups
integer
Upvalue count.
addr
string
Address of the closure in the game, as hex text like 0x1A2B3C40.
env
table
Table proxy for the closure's environment. Absent when the environment is not a readable table with a hash part.
proto
string
Address of the function prototype, as hex text. Only present for Lua closures.
debug.getupvalue(fn, index)
Reads one upvalue of a game closure. An upvalue captured by reference is followed to the shared cell it points at. Only the value is returned, not a name.
Parameters:
fn: table - A function proxy.
index: integer - Upvalue number, from 1 to the closure's upvalue count.
Returns:
value: any - nil, a boolean, a number, a string, a table proxy or a function proxy. Other value types and unreadable cells give nil. Also nil when fn is stale.
Raises upvalue index out of range (the function has N) when index is outside 1..N.
lua
local closures = getclosures() or {}
local fn = closures[1]
if fn and debug.getinfo(fn) then
print(debug.getupvalue(fn, 1))
end
debug.getupvalues(fn)
Reads every upvalue of a game closure into an array, converted the same way as debug.getupvalue. Not part of standard Lua.
Parameters:
fn: table - A function proxy.
Returns:
upvalues: table - Array indexed 1..N. Empty when fn is stale.
Upvalues that read as nil leave holes, so use debug.getinfo(fn).nups for the count rather than #.
lua
for _, fn in ipairs(getclosures() or {}) dofor index, value in pairs(debug.getupvalues(fn)) doif value == "Sniper"then
print(debug.getinfo(fn).addr, index)
endendend
debug.setupvalue(fn, index, value)
Overwrites one upvalue of a game closure in game memory. For an upvalue captured by reference the shared cell is written, so every closure sharing that variable sees the change.
Parameters:
fn: table - A function proxy.
index: integer - Upvalue number, from 1 to the closure's upvalue count.
value: nil | boolean | number | table - New value: nil, a boolean, a number, a table proxy or a function proxy. It is required, so pass nil explicitly to clear.
Returns:
ok: boolean - true after the write. false when fn is stale.
Strings, local tables, local functions and other types raise only nil, booleans, numbers and game objects can be written.
Changing a collectable upvalue to a different collectable type is refused.
Other errors: upvalue index out of range (the function has N), that upvalue slot is not readable, that upvalue is not readable, could not read that upvalue and could not write that upvalue.
lua
for _, fn in ipairs(getclosures() or {}) dolocal values = debug.getupvalues(fn)
if values[1] == "Sniper"and type(values[2]) == "number"then
print("old cooldown", values[2])
debug.setupvalue(fn, 2, 0)
endend
Overrides
rawget(table, key)
The global rawget is wrapped. When table is a table proxy and key is a string, the game table is read exactly like proxy[key]. In every other case the original rawget runs unchanged.
Parameters:
table: table - Any table, or a table proxy.
key: any - Key to read. Both arguments are required.
Returns:
value: any - The raw value.
Number keys are not converted here, so rawget(proxy, 1) reads the empty proxy and returns nil, while proxy[1] looks up the string key "1".
lua
for _, config in findgc("MaxAmmo") do
print(rawget(config, "MaxAmmo"))
breakend
FFlags
Read and change Roblox fast flags (FFlags) in the running client, and force the sky to re-render.
setfflag needs the unsafe API
setfflag raises setfflag requires Allow unsafe Lua API. unless Allow unsafe Lua API is on (see Sandbox & Safety). getfflag and reloadsky work without it.
Memory only, not saved
setfflag changes the live value in Roblox only. It is lost when Roblox restarts and not undone by a VM reset. Some flags only take effect after the client re-reads them.
Exact names
Names are exact and case-sensitive, without the FFlag/FInt prefix, for example TaskSchedulerTargetFps.
fps-cap.lua
local name = "TaskSchedulerTargetFps"local current = getfflag(name)
print(name, "is", current)
if math.type(current) == "integer"thenlocal ok, result = pcall(setfflag, name, 144)
if ok and result then
notify("FPS cap set to 144")
else
warn("setfflag failed:", result)
endend
Flag types
Each flag in the registry is classified by its variable type. What getfflag returns and what setfflag accepts depends on that type:
Flag type
getfflag returns
setfflag
bool (regular and Luau bool flags)
boolean
Accepts true/false, the numbers 1/0, or the strings "true"/"false" (any case) and "1"/"0".
int (regular and Luau int flags)
integer (signed 32-bit)
Accepts a whole number from -2147483648 to 2147483647, or a decimal string of digits with an optional leading + or - (at most 11 characters). Floats with a fraction, NaN and booleans are rejected.
string
string, or nil when the text is longer than 4096 bytes or cannot be read
Not settable: raises <name> is not a bool or int flag, so it cannot be set.
exists only in the remote config
the raw config text as a string (or nil if it cannot be read)
Raises <name> exists only in the remote config and has no variable in this client.
unrecognised type
nil
Raises <name> is not a bool or int flag, so it cannot be set.
Functions
getfflag(name)
Looks up a fast flag by name and returns its current value from Roblox memory, converted by the flag's type.
Parameters:
name: string - Exact, case-sensitive flag name.
Returns:
value: boolean | integer | string | nil - The flag's value, or nil when no flag has that name, the type is not recognised, or the value cannot be read.
An unknown name returns nil; it does not raise.
Raises one of the registry errors when Roblox is not attached or the flag registry cannot be used.
lua
local value = getfflag("TaskSchedulerTargetFps")
if value == nilthen
print("flag not found")
else
print("TaskSchedulerTargetFps =", value)
end
setfflag(name, value)
Writes a new value into a bool or int fast flag, then reads it back. Values are converted as described in Flag types.
Requires the unsafe API.
Parameters:
name: string - Exact, case-sensitive flag name.
value: boolean | number | string - The new value. Omitting it or passing nil raises setfflag needs a value.
Returns:
ok: boolean - true when reading the flag back gives the value that was written, false otherwise.
Raises Unknown flag: <name> when no flag has that name.
Raises <name> is a bool flag; use true, false, 1 or 0. or <name> is an int flag; use a whole number that fits 32 bits. when the value cannot be converted.
Raises <name> has no valid storage. when the flag's variable is not inside the Roblox module.
local ok, err = pcall(setfflag, "TaskSchedulerTargetFps", "240")
ifnot ok then
warn(err)
end
reloadsky()
Makes Roblox rebuild the skybox and lighting on the next frame.
Does nothing when Roblox is not attached, when this Roblox build isn't supported yet, or when the render view cannot be found.
lua
reloadsky()
Errors
Both getfflag and setfflag raise these when the flag registry cannot be used:
Message
Meaning
Roblox is not attached.
No Roblox process is attached.
The flag registry is not available on this Roblox build.
This Roblox build isn't supported yet.
The flag registry is not initialised yet.
The registry pointer is not set yet, for example very early in startup.
The flag registry layout does not match this Roblox build.
The registry does not look like a valid flag table on this build.
Teleport
Queue Lua source that runs in a fresh VM after Roblox moves you to another server or place.
The VM restarts
After a teleport, clover starts a fresh VM before running queued scripts. Nothing from the old VM survives, so queued scripts must set everything up again.
Nothing queued, no restart
If the queue is empty, a teleport does not touch the VM. Your scripts keep running, and the game and workspace globals are pointed at the new DataModel as soon as it appears.
Queue before you teleport
Scripts aren't compiled when queued. Check them with load first so a syntax error doesn't waste the teleport.
after-teleport.lua
local source = [[
print("arrived in place", game.PlaceId, "server", game.JobId)
notify("teleport script ran")
]]local compiled, problem = load(source)
ifnot compiled then
error(problem)
end
queue_on_teleport(source)
print("queued; it will run after the next teleport")
What happens on a teleport
While at least one script is queued, the engine samples the game on every pass of the script thread (roughly every 16 ms, only while the master switch is on). It counts a teleport when any of these change compared with the game it saw before:
the DataModel, the LocalPlayer, game.PlaceId or game.GameId (each only when both the old and new values are known)
workspace.DistributedGameTime jumps back by more than 5 seconds
the DataModel disappears for at least 0.25 seconds and then comes back
Wait for the new game
After a teleport is detected, the engine waits until the DataModel, the LocalPlayer and game.PlaceId have all been present for 1.5 seconds without interruption. If that never happens, it goes ahead anyway 30 seconds after the teleport was detected.
Take the queue
Every queued script is removed from the queue, which is now empty. The log shows teleport detected; restarting the VM to run the queued scripts.
Restart the VM
On its next pass the script thread closes the VM, opens a fresh one with the full API and clears the drawing buffers. The log shows vm reset complete and running N queued teleport script(s).
Run the scripts
The scripts run once each, in the order they were queued, as text chunks in their own coroutines, so they may yield with task.wait. A compile or runtime error is logged and the next script still runs. A script that should also run after the following teleport has to queue itself again.
The queue is emptied without running anything when the attached Roblox process changes, when no game is loaded for 120 seconds, when clear_teleport_queue is called, or when the VM is reset for any other reason, including turning the unsafe API toggle on or off.
Limits
Limit
Value
Error when exceeded
Scripts in the queue
32
queue_on_teleport already holds 32 scripts
Size of one script
256 KB (262144 bytes)
queue_on_teleport script is larger than 256 KB
Total size of the queue
1 MB (1048576 bytes)
queue_on_teleport queue is larger than 1 MB
Empty source
not allowed
queue_on_teleport needs a non-empty script
Timing
Value
New game must be stable for
1.5 s
Run anyway after
30 s
Queue dropped after no game for
120 s
Shortest DataModel gap that counts as a teleport
0.25 s
DistributedGameTime drop that counts as a teleport
more than 5 s
Functions
queue_on_teleport(source)
Adds Lua source to the teleport queue. After the next detected teleport the VM restarts and runs every queued script once. Returns nothing.
Parameters:
source: string - Lua source text. It is stored as-is and only compiled when it runs, as a text chunk (precompiled bytecode is not accepted).
Aliases:queueonteleport
Raises an error for empty source or when a limit is exceeded.
The queue lives outside the Lua VM, so it survives the VM restart it triggers, but a manual VM reset empties it.
lua
if isfile("rejoin.lua") then
queue_on_teleport(readfile("rejoin.lua"))
end
clear_teleport_queue()
Removes every queued script and stops watching for a teleport until something is queued again. Takes no arguments and returns nothing.
Because each queued script runs only once, a script that should follow you through several teleports re-queues itself. Keep it in the workspace folder (see file) and queue it from the file:
rejoin.lua
print("now in place", game.PlaceId)
queue_on_teleport(readfile("rejoin.lua"))
Compatibility shims
Fallbacks for function names other executors provide, and what each really does here.
How the shims are defined
Shims are defined as name = name or fallback at startup, so they never replace engine functions. You can overwrite them.
Several shims are approximations
Several are approximations: getrawmetatable respects __metatable, gethiddenproperty can't see hidden properties, rconsole* writes to the normal log, and fireclickdetector does nothing.
compat-check.lua
print(getexecutorname(), getfpscap())
print("print is a C function:", iscclosure(print))
print("same object:", compareinstances(game:GetService("Players"), game:FindFirstChild("Players")))
local helpers, problem = loadfile("lib/helpers.lua")
if helpers then
helpers()
else
rconsolewarn("helpers not loaded: " .. tostring(problem))
end
Metatables and closures
getrawmetatable(object)
Returns getmetatable(object). This is not a raw read: if the metatable has a __metatable field, that field is returned instead of the metatable.
Parameters:
object: any - Any value.
Returns:
table | any | nil - The metatable, the value of its __metatable field, or nil when there is no metatable.
Engine objects (Instances, Vector3, Drawing objects and so on) have no __metatable field, so for them this is the real metatable.
With the unsafe API on, the full debug library is loaded and debug.getmetatable(object) bypasses __metatable.
lua
local mt = getrawmetatable(Vector3.new())
print(type(mt.__index), type(mt.__add))
setrawmetatable(object, metatable)
Sets the metatable and returns object. Falls back to debug.setmetatable when a locked metatable blocks setmetatable.
Parameters:
object: any - The value whose metatable to set.
metatable: table | nil - The new metatable, or nil to remove it.
Returns:
any - object.
The fallback needs the unsafe API. Without it, protected tables and non-tables raise an error.
lua
local point = setrawmetatable({x = 1}, {__tostring = function(self) return"point " .. self.x end})
print(tostring(point))
compareinstances(a, b)
Returns a == b. Two Instance values compare equal when they wrap the same Roblox object address, so this is the same as using == on Instances in clover.
Parameters:
a: any - First value.
b: any - Second value.
Returns:
boolean - Whether the values are equal.
lua
local players = game:GetService("Players")
print(compareinstances(players, game:FindFirstChild("Players")))
islclosure(func)
Returns true if func is a Lua function. It works by calling string.dump(func) in pcall, which only succeeds for Lua functions.
Parameters:
func: any - The value to test.
Returns:
boolean - true for Lua functions, false for C functions and for anything that is not a function.
rconsoleerr is the same function, so error text is logged as a warning. Use clover.error to log at error severity.
rconsoleclear()
Does nothing. The clover log is not cleared.
rconsolename([title])
Does nothing; there is no console window to rename.
Parameters:
title: string (optional) - Ignored.
Aliases:rconsolesettitle
rconsoleinput()
Returns an empty string immediately. It does not wait for input.
Returns:
string - Always "".
printconsole(...)
The same function as the global print, captured when the VM starts.
Parameters:
...: any - Values to log.
Frame rate
setfpscap([fps])
Writes fps to the TaskSchedulerTargetFps fast flag through setfflag. Returns nothing and never raises: every failure is swallowed by pcall.
Requires the unsafe API.
Parameters:
fps: number (optional) - Whole-number frame rate. nil, 0 or negative means 9999.
Needs the unsafe API toggle. Without it setfflag refuses and setfpscap silently does nothing.
Also does nothing when Roblox is not attached, or when the flag cannot be found or is not an integer flag.
lua
setfpscap(144)
print("cap is now", getfpscap())
getfpscap()
Reads the TaskSchedulerTargetFps fast flag with getfflag. Works without the unsafe API.
Returns:
number - The flag value, or 60 when it cannot be read (not attached, flag not found, or not a number). The 60 is a fixed fallback, not a measured cap.
Instances
fireclickdetector(detector, [distance], [event])
Does nothing in practice: clover Instances don't expose ClickDetector events.
Parameters:
detector: Instance - The ClickDetector.
distance: number (optional) - Ignored.
event: string (optional, default "MouseClick") - Name looked up on the detector.
To click one, use firesignal: firesignal(detector, "MouseClick", player).
It reads game:GetService("Players").LocalPlayer first, so it raises an error while no game is attached.
lua
local button = workspace:FindFirstChild("Button")
local detector = button and button:FindFirstChildOfClass("ClickDetector")
if detector then
firesignal(detector, "MouseClick", game:GetService("Players").LocalPlayer)
end
gethiddenproperty(object, name)
Returns object[name], false. It can't read hidden properties, only what the bridge supports.
Parameters:
object: Instance - The object to read from.
name: string - Property name.
Returns:
value: any - The looked-up value, or nil.
isHidden: boolean - Always false.
lua
local value = gethiddenproperty(workspace, "DistributedGameTime")
print(value)
sethiddenproperty(object, name, value)
Runs object[name] = value inside pcall and returns whether it raised no error.
Parameters:
object: Instance - The object to write to.
name: string - Property name.
value: any - New value.
Returns:
boolean - true when the assignment did not raise. Instances ignore unknown property names without an error, so true does not prove that anything was written.
getinstances()
Returns game:GetDescendants(): every Instance under the DataModel, in depth-first order. Returns an empty table when that call fails, for example while no game is attached.
Returns:
table - Array of Instances, at most 50000 (the GetDescendants limit).
Instances parented to nil are not included; getnilinstances does not exist.
lua
local count = 0for _, instance in ipairs(getinstances()) doif instance.ClassName == "RemoteEvent"then count = count + 1endend
print(count, "RemoteEvents")
Encoding aliases
The shims add these extra names for the base64 helpers. They are documented in full on the crypt page.
A 44-character base64 string of 32 random bytes. See crypt.generatekey.
Clipboard
There is no global setclipboard or toclipboard. The shims only define toclipboard when a global setclipboard already exists, and none does. Use utility.SetClipboard(text) (the same function as clover.setclipboard), which needs the unsafe API. See utility.
Not available
These common executor functions are not defined at all, so calling them raises attempt to call a nil value. Shared scripts should check first, for example if hookfunction then ... end.