Lifecycle and cleanup
Re-running your script, tearing a window down, and disconnecting everything cleanly.
Re-running replaces the old window#
Running your script again destroys its previous window and opens a fresh one:
local win = Ember.new({ Title = "My Script" })A window is matched by identity, which is its Name, or its Title when there
is no Name. Only windows with the same identity are replaced, so loading a
second Ember script does not tear down the first one's UI.
To keep the old window on screen:
local win = Ember.new({ Title = "My Script", ReplaceExisting = false })Two windows built from the same script, without a Name to tell them apart,
will replace each other. Give them different names:
local a = Ember.new({ Name = "main", Title = "My Script" })
local b = Ember.new({ Name = "debug", Title = "My Script" })One key, one window
Windows bound to the same toggle key would all dissolve on one press, so the newest window using that key is the only one that answers it. Windows on different keys each answer their own.
Destroying a window#
win:Destroy()In order, this flushes any pending save, runs the RevertOnClose callbacks,
runs your OnDestroy handlers, disconnects everything tracked, and removes the
GUI once the dissolve has played. It returns true, or false if the window
was already destroyed.
Cleaning up your own work#
Ember cannot see connections your script made. Register them and they are torn down with the window:
win:OnDestroy(function()
heartbeat:Disconnect()
myEspFolder:Destroy()
end)You can register as many as you like. They run in registration order, each in
its own pcall, so one that errors warns and the rest still run. OnDestroy
returns a subscription with :Disconnect() if you want to unregister one. On a
window that is already destroyed, the handler runs on the next frame instead.
To hand a connection straight to the window, use Track:
win:Track(game.Players.PlayerAdded:Connect(onPlayerAdded))Track returns what you gave it. It takes a connection, an instance, or a plain
function to call on the way out. Tracking something on an already-destroyed
window disposes it immediately.
Surviving a respawn#
A script that sets WalkSpeed, or hooks something on the Humanoid, loses that
the moment the player dies, resets, or the game swaps their character. The
control still reads "on" while the effect is gone.
Reapply re-fires a control's callback with its current value once the new
character is ready:
main:Slider({
Text = "Walk speed",
Min = 16, Max = 250, Default = 16,
Reapply = true,
Callback = function(value)
local char = game.Players.LocalPlayer.Character
local hum = char and char:FindFirstChild("Humanoid")
if hum then hum.WalkSpeed = value end
end,
})The value re-applied is whatever the control holds at that moment, including one
restored by Save.
For every control in a window, set it once and let a control opt out:
local win = Ember.new({ Title = "My Script", ReapplyOnRespawn = true })
main:Toggle({ Text = "Noclip", Reapply = false }) -- stays out of itCustom character models are covered
The trigger is CharacterAdded, which Roblox fires whenever Character is
assigned, including a model a game builds itself. Ember waits up to five seconds
for a Humanoid and then re-applies regardless.
Running your own code on respawn#
win:OnRespawn(function(character)
character:WaitForChild("Humanoid").JumpPower = 120
end)The callback receives the new character. It returns a subscription with
:Disconnect(), the same shape as OnDestroy.
To push every opted-in control's value into the game right now, without waiting for a respawn:
win:Reapply()Undoing what a control did, on close#
Closing the window tears down everything Ember made. It cannot know what your
callback did: an ESP folder, a highlight on every player, a loop your script
started. That code already exists as the "off" branch of the same callback.
RevertOnClose runs it.
main:Toggle({
Text = "ESP",
RevertOnClose = true,
Callback = function(on)
if on then buildEsp() else clearEsp() end
end,
})Close the window and the toggle's callback is called with false, so
clearEsp() runs.
OnEnabled and OnDisabled split that branch into two functions:
main:Toggle({
Text = "ESP",
RevertOnClose = true,
OnEnabled = buildEsp,
OnDisabled = clearEsp,
})Callback still runs first, then OnEnabled when the value is true or
OnDisabled when it is false. They work on any control that reports a
boolean, custom controls included. Each of the three runs even if another
errors, and an error warns rather than stopping the rest.
A control whose current value is a boolean reverts to false. Anything else
reverts to its Default, and RevertTo overrides both:
main:Slider({
Text = "Walk speed",
Min = 16, Max = 250, Default = 16,
RevertOnClose = true,
RevertTo = 16,
Callback = setWalkSpeed,
})A non-boolean control with no Default and no RevertTo has nothing to revert
to, so nothing is called.
For a whole window, with a control able to opt out:
local win = Ember.new({ Title = "My Script", RevertOnClose = true })
main:Toggle({ Text = "Dark theme", RevertOnClose = false }) -- nothing to undoReverts run before the OnDestroy handlers and before any tracked connection is
disposed, so a revert can still use the things it is undoing.
Destroying a control unregisters it from both Reapply and RevertOnClose.
This is not a substitute for Track
Reverting calls your callback; it does not find and remove things by itself. For
anything that is not a control, like a Heartbeat connection or a folder you made
at startup, hand it to the window with Track or clean it up in OnDestroy.
Only controls that carry a value
Reapply and RevertOnClose push a value at Callback, which only works where
Callback means "here is the new value": Toggle, Slider, Input,
Dropdown, ColorPicker, Palette and Status. On a Keybind it means "the
bound key was pressed" and on a Button "it was clicked", so both are skipped
with a warning. Give one an Apply = function(value) … end to opt it in, and
that function is called instead of Callback.
Teleports#
A teleport to another server ends the Lua state your script is running in, so
nothing in Ember survives it. What survives is the script being run again, which
your executor does with queueonteleport:
local source = "loadstring(game:HttpGet('https://rbx.lol/ember.lua'))() -- your script here"
if queueonteleport then queueonteleport(source) endWhen the script runs again on the new server it rebuilds its window. With Save
on your controls they come back with the values they had, and win:Reapply()
pushes those values into the fresh character in one call.
Not every executor has it
queueonteleport is an executor function, not a Roblox one, and the name varies
(queue_on_teleport on some). Check for it before calling it, as above.
A complete script#
Track connections, tear them down on destroy, and exit from a danger button. For a script with every control wired to something in-game, see the full showcase.
local Ember = loadstring(game:HttpGet('https://rbx.lol/ember.lua'))()
local win = Ember.new({ Title = "My Script", Subtitle = "v1.0" })
local main = win:Section("Main", "home")
local running = true
-- Managed by the window: gone when it is.
win:Track(game:GetService("RunService").Heartbeat:Connect(function()
if not running then return end
-- per-frame work
end))
main:Toggle({
Text = "Enabled",
Default = true,
Callback = function(on) running = on end,
})
main:Button({
Text = "Close",
ButtonText = "Exit",
Danger = true,
Callback = function() win:Destroy() end,
})
win:OnDestroy(function()
running = false
print("cleaned up")
end)What survives a destroy#
Saved settings. They live on disk, not in the window, so destroying a window and
running the script again restores every control with a Save key, plus the
window's size, position and rail width if you set SaveLayout = true. See
Saving settings.
To wipe them:
Ember.Persist.forget() -- every saved control value
Ember.Persist.set("godmode", nil) -- one key, marked as clearedEmber.Store.clear(name) is a different thing: it deletes the file name.json.
Control values are keys inside the one settings file, so Store.clear does not
reach them.
Checking whether a window is still alive#
Live windows are registered in getgenv(), or _G where there is no getgenv:
local live = getgenv().__EMBER_LIVE -- array of live windows, or nil
print(live and #live or 0)This is what ReplaceExisting reads to find a previous window with the same
identity. The key is removed once the last window is destroyed.