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:

Luau
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:

Luau
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:

Luau
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#

Luau
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:

Luau
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:

Luau
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:

Luau
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:

Luau
local win = Ember.new({ Title = "My Script", ReapplyOnRespawn = true })
 
main:Toggle({ Text = "Noclip", Reapply = false })  -- stays out of it

Custom 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#

Luau
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:

Luau
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.

Luau
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:

Luau
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:

Luau
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:

Luau
local win = Ember.new({ Title = "My Script", RevertOnClose = true })
 
main:Toggle({ Text = "Dark theme", RevertOnClose = false })  -- nothing to undo

Reverts 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:

Luau
local source = "loadstring(game:HttpGet('https://rbx.lol/ember.lua'))() -- your script here"
if queueonteleport then queueonteleport(source) end

When 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.

Luau
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:

Luau
Ember.Persist.forget()              -- every saved control value
Ember.Persist.set("godmode", nil)   -- one key, marked as cleared

Ember.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:

Luau
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.