v0.13.8

Saved variables

main · 4b0211c · latest release v0.13.8source on GitHub ↗

wowlua:saved keeps settings and data between sessions in typed SavedVariables, with defaults, profiles and migrations. This page shows how to bind one, when it can be read, and how to change its shape in an update.

Save your settings

Declare the SavedVariable in the manifest, as you would list it in a TOC file. wowlua writes the TOC from it.

wowlua.jsonjson
{  "$schema": "https://raw.githubusercontent.com/DomenikIrrgang/wowlua/main/schema/wowlua-0.1.json",  "package": {    "name": "my-addon",    "version": "0.1.0",    "wowlua": "^0.13"  },  "addon": {    "title": "MyAddon",    "saved-variables": ["MyAddonDB"]  }}

Describe its shape with an interface and bind it with saved.bind, giving a version and the defaults. bind returns the data table itself, typed as the interface.

src/db.wowluawowlua
import * as saved from "wowlua:saved" export interface Settings    scale: number    showFreeSlots: boolean    position?: { x: number, y: number }    hidden: { [string]: boolean }end export local db = saved.bind("MyAddonDB", {    version = 1,    defaults = { scale = 1, showFreeSlots = true, hidden = {} } :: Settings,})

Other files import db and use it. A misspelled field such as db.showFreeSlot, or a string assigned to db.scale, is a compile error. The name passed to bind must be a string literal (E8102) naming a SavedVariable the manifest declares (E8020), and each SavedVariable can be bound once (E8103).

Read and write it like any table

There are no getters or setters. Reading a field gives the stored value, or its default when nothing is stored. Assigning a field stores the value.

src/window.wowluawowlua
import onLoad from "wowlua:lifecycle"import db from "./db" local frame = CreateFrame("Frame", nil, UIParent) @onLoadlocal function restore()    frame:SetScale(db.scale)                  -- 1 on a fresh install    if db.position then        frame:SetPoint("BOTTOMLEFT", db.position.x, db.position.y)    endend export function savePosition()    local x, y = frame:GetLeft(), frame:GetBottom()    if issecretvalue(x) or issecretvalue(y) then        return                                -- a secret position can't be stored    end    db.position = { x = x, y = y }end export function hideCategory(name: string)    db.hidden[name] = true                    -- stored in the saved dataend

Defaults are attached lazily, nested tables included. The first read of db.hidden creates a new table in the saved data, with the nested defaults attached to it, so writes into it never change the shared defaults.

Only what changed is written to disk

At logout, values equal to their defaults are removed before WoW writes the file, nested tables included, key by key. The file stays small. When an update changes a default, players who never changed that setting get the new one.

After a player sets the scale to 1.2 and hides one category, WoW writes:

lua
MyAddonDB = {	["scale"] = 1.2,	["hidden"] = {		["Junk"] = true,	},	["__version"] = 1,}

showFreeSlots is missing because it still has its default. To keep every value, bind with stripDefaults = false. __version is reserved: the module stores the data's version under it.

When the data is ready

WoW loads SavedVariables after the addon's files have run. You can call bind at the top of a file, but the table it returns stays locked until the data has arrived. Any read or write before that raises an error, in every build, since a write before load would be lost:

text
MyAddon: MyAddonDB accessed before it was loaded; use @onLoad

So use the table in @onLoad or later. By then the module has loaded the stored data into the same table, run the migrations and attached the defaults. If the addon needs its data while its files load, set "load-saved-variables-first": true under addon in the manifest, and the table is usable at the top level.

Let players keep several profiles

Profiles let a player share one setup across characters, or keep several. Bind with profiles = true, and declare a per-character SavedVariable named after the first with PerChar appended. It remembers which profile each character uses (E8021 when it is missing).

json
"addon": {  "title": "MyAddon",  "saved-variables": ["MyAddonDB"],  "saved-variables-per-character": ["MyAddonDBPerChar"]}
src/db.wowluawowlua
import * as saved from "wowlua:saved" export interface Settings    scale: number    showFreeSlots: boolean    position?: { x: number, y: number }    hidden: { [string]: boolean }end export local db = saved.bind("MyAddonDB", {    version = 1,    defaults = { scale = 1, showFreeSlots = true, hidden = {} } :: Settings,    profiles = true,})

db now always shows the active profile, and the same table keeps working after a switch. Profile housekeeping lives on a separate handle, saved.handle(db), so your own fields can have any name, reset and profiles included.

src/profiles.wowluawowlua
import * as saved from "wowlua:saved"import print from "wowlua:log"import db from "./db" export function manageProfiles()    local profiles = saved.handle(db).profiles    if not profiles then        return    end    profiles:use("Healer")                -- switch, created empty when new    print("active:", profiles:current())  -- "Default" until the first switch    for _, name in ipairs(profiles:list()) do        print(" -", name)    end    profiles:copy("Default")              -- replace this profile's data with a copy    profiles:delete("Old")                -- any profile but the active one     profiles:onChange(function(name: string)        print("now using", name, "at scale", db.scale)    end)end

handle(db).profiles is nil for a binding without profiles. list() returns the names sorted. copy of a profile that doesn't exist and delete of the active profile are errors. use, list, copy, delete and reset raise an error before the data is loaded.

Change the shape of your data

When an update renames or restructures a field, raise version and add a migration for the new version. migrate[v] upgrades data saved at version v - 1 and changes it in place.

src/db.wowluawowlua
import * as saved from "wowlua:saved" export interface Settings    scale: number    showFreeSlots: boolean    position?: { x: number, y: number }    hidden: { [string]: boolean }end export local db = saved.bind("MyAddonDB", {    version = 3,    defaults = { scale = 1, showFreeSlots = true, hidden = {} } :: Settings,    profiles = true,    migrate = {        -- 2: "uiScale" was renamed to "scale"        [2] = function(old: any)            for _, profile in pairs(old.profiles or {}) do                profile.scale = profile.uiScale                profile.uiScale = nil            end        end,        -- 3: "hidden" became a table        [3] = function(old: any)            for _, profile in pairs(old.profiles or {}) do                profile.hidden = profile.hidden or {}            end        end,    },})
  • Migrations run at load, from the stored version plus one up to version, in order. Data saved at version 1 gets both of these.
  • A fresh install, with nothing stored, runs none.
  • Each migration gets the whole stored table once, every profile included, which is why the example loops over old.profiles.
  • Migrations run on a copy. When one fails, the error is reported, the stored data stays as it was, nothing is stripped at logout, and using the table raises an error such as MyAddon: MyAddonDB failed to migrate to version 3; data preserved. Ship a fix, and the player's data is still there.
  • Data saved by a newer version of your addon is left untouched: no migration runs, nothing is stripped, and a warning is reported once. A player who goes back to an older version loses nothing.

Reset to defaults

Resetting is one call. With profiles it resets the active profile, without profiles the whole data.

wowlua
saved.handle(db):reset()

saved.handle(db):isLoaded() tells whether the data has been loaded, for code that may run before or after ADDON_LOADED.

Data per character

Data that belongs to one character, such as that character's bag contents, goes in a SavedVariable listed under saved-variables-per-character. Bind it the same way. Profiles are for account-wide data, so profiles = true on a per-character SavedVariable is an error (E8117).

wowlua.jsonjson
{  "$schema": "https://raw.githubusercontent.com/DomenikIrrgang/wowlua/main/schema/wowlua-0.1.json",  "package": {    "name": "my-addon",    "version": "0.1.0",    "wowlua": "^0.13"  },  "addon": {    "title": "MyAddon",    "saved-variables": ["MyAddonDB"],    "saved-variables-per-character": ["MyAddonDBPerChar", "MyAddonCharDB"]  }}
src/inventory.wowluawowlua
import * as saved from "wowlua:saved" interface CharData    gold: number    items: { [number]: number }end export local charDb = saved.bind("MyAddonCharDB", {    version = 1,    defaults = { gold = 0, items = {} } :: CharData,})

At a glance

Call What it does
saved.bind(name, options) Binds a SavedVariable: version, defaults, and optionally migrate, profiles and stripDefaults. Returns the data table.
saved.handle(db) The housekeeping handle of a bound table
handle:reset() The active profile, or the whole data, back to the defaults
handle:isLoaded() Whether the data has been loaded
handle.profiles use, current, list, copy, delete and onChange; nil without profiles

The defaults' type may not contain secret values (E8104), since they can't be compared or saved. The full rules are in the specification.

main · 4b0211csource on GitHub ↗