Saving settings

One option per control, and the value survives between sessions.

One option#

Give any stateful control Save = true. Ember writes the value to disk and reads it back the next time your script runs.

Luau
main:Toggle({
    Text = "God mode",
    Save = true,
    Callback = function(on) godmode = on end,
})

You do not name the key. Ember builds one from where the control sits — Window/Section/Group/Kind/Text — which is unique without you having to invent anything or remember what you already used.

A restored value calls your Callback#

When a control's value comes off disk, Ember calls that control's Callback once with the restored value. The switch comes back on and your variable holds true, so the control and your script cannot disagree.

This is separate from FireOnStart, which fires the callback with whatever the control holds even when nothing was saved.

The initial calls run in creation order, one control at a time, so a control built later sees the state an earlier restore already applied. Each call is wrapped, and a callback that errors warns with [Ember] initial callback: rather than stopping the rest of the window from building.

Controls that get the call: Toggle, Slider, Input, Dropdown, ColorPicker, Palette and Status.

Keybind is the exception

A Keybind's Callback means "the bound key was pressed", not "here is a new value". A saved keybind is restored to its key without the callback running.

What can be saved#

Toggle, Slider, Input, Dropdown, Keybind, ColorPicker, Palette and Status. Buttons, titles and separators hold no value, so Save does nothing on them.

Luau
main:Slider({ Text = "Speed", Min = 16, Max = 250, Save = true })
main:Dropdown({ Text = "Theme", Options = Ember.ThemeNames(), Save = true })
main:ColorPicker({ Text = "ESP", Save = true })

What the derived key looks like#

Window/Section/Group/Kind/Text. The group segment is left out for a control that is not in one, and lower-cased when it is there. The window segment is the window's Name, or its Title when there is no Name.

Luau
local win  = Ember.new({ Title = "My Script" })
local main = win:Section("Main", "home")
 
main:Toggle({ Text = "God mode", Save = true })
-- key: "My Script/Main/Toggle/God mode"
 
local advanced = main:Group({ Text = "Advanced" })
advanced:Toggle({ Text = "Debug logging", Save = true })
-- key: "My Script/Main/advanced/Toggle/Debug logging"

Being built from the window means two scripts on one executor cannot overwrite each other, and being built from the section and group means the same label in two places stays two values.

When to write the key yourself#

There is one thing the derived key cannot survive: renaming. Change the window, the section, the group or the label and the key changes with it, so the old value is orphaned and the control comes back at its Default. Nothing breaks, but the setting quietly resets.

So write a key for the handful of settings that would annoy someone to lose, and leave the rest on Save = true:

Luau
main:Toggle({ Text = "God mode", Save = "godmode" })

A string means exactly itself — no window, section or label goes into it — so you can rename or move the control freely and rearrange the whole window without anyone losing what they had set.

Where it goes#

By default a folder called ember in your executor's workspace, holding settings.json. Change either with Ember.Configure:

Luau
Ember.Configure({
    Storage = {
        Folder = "myscript",     -- workspace/myscript/
        File   = "config",       -- workspace/myscript/config.json
    },
})

Call Configure before Ember.new, so the first window already knows where to look.

OptionTypeDefaultWhat it does
Folderstring"ember"Folder under the executor's workspace.
Filestring"settings"File name inside that folder, written as .json.

Enabled, Debounce, Unique and Owner are in the configuration reference.

Two scripts, one folder#

Every Ember script on the executor shares one settings file unless you say otherwise, so give each script its own folder:

Luau
Ember.Configure({
    Storage = { Folder = "bloxburg-helper", Unique = true },
})

Unique = true claims the folder by writing a .owner file into it. If another owner already holds it, yours becomes bloxburg-helper_2, then _3, up to 50 attempts. Owner sets the identity written into that file and defaults to the folder name.

Claiming needs isfolder, makefolder and writefile. Without them the claim fails and nothing is saved that session.

Writes are debounced#

Dragging a saved slider does not write on every frame. Writes are collected and flushed 0.4 seconds after the last change. Raise it if you are writing a lot:

Luau
Ember.Configure({ Storage = { Debounce = 1.0 } })

Destroying a window flushes any pending write first, so re-running your script within the debounce does not lose the last change.

When there is no filesystem#

Saving needs writefile, readfile and isfile. If the executor is missing any of the three, Ember runs without persistence. The window works and controls still fire; nothing is kept between runs.

Luau
Ember.Store.available()   --> true when those three exist

isfolder and makefolder are used when they exist. delfile is needed to delete anything.

To turn saving off:

Luau
Ember.Configure({ Storage = { Enabled = false } })
Ember.Configure({ Storage = false })          -- the same thing, shorter

Enabled = false stops reads as well as writes, so controls start from their Default.

When a write fails anyway#

Store.available() says the functions exist, not that they work. A failed read or write warns once per session and stores the reason:

Luau
Ember.Store.lastError   --> nil, or why the last read or write failed

A settings file that will not parse is copied to settings.json.corrupt before anything else touches it. The controls fall back to their defaults for that run.

Reading and writing directly#

For state with no control behind it, like a list of favourites or a session counter, Ember.Store writes a file of its own in the same folder, one file per name:

Luau
Ember.Store.set("favourites", { "Kitchen", "Attic" })
 
local favourites = Ember.Store.get("favourites")   --> table
 
Ember.Store.clear("favourites")   -- deletes favourites.json

Values must be JSON-encodable: strings, numbers, booleans, and tables of those. Anything outside A-Z a-z 0-9 _ - in the name becomes _ in the file name.

Inside the settings file#

Ember.Persist is the layer every Save key goes through. Control values are keys inside one file, so Store.clear cannot reach an individual control and Persist can.

CallWhat it does
Ember.Persist.get(key, fallback)Reads one saved value
Ember.Persist.set(key, value)Writes one, debounced. nil marks it cleared, so it reads back as nil instead of falling through to a Default.
Ember.Persist.flush()Writes now instead of waiting out the debounce
Ember.Persist.forget()Deletes the settings file and empties the in-memory copy
Ember.Persist.key(opts, section, kind)The key a control's options would produce
Ember.Persist.wasRestored(key)Whether that key came off disk, rather than falling back to a default

Writes merge over what is on disk, so one script saving does not drop the keys another script wrote to the same file.

A pair of housekeeping buttons:

Luau
settings:Button({
    Text = "Flush saved settings",
    ButtonText = "Flush",
    Callback = function() Ember.Persist.flush() end,
})
 
settings:Button({
    Text = "Clear saved settings",
    ButtonText = "Forget",
    Danger = true,
    Callback = function() Ember.Persist.forget() end,
})

Controls already on screen keep their values after forget(). The file is what was emptied. They reload from Default on the next run.