--- **AceDB-3.0** manages the SavedVariables of your addon. -- It offers profile management, smart defaults and namespaces for modules.\\ -- Data can be saved in different data-types, depending on its intended usage. -- The most common data-type is the `profile` type, which allows the user to choose -- the active profile, and manage the profiles of all of his characters.\\ -- The following data types are available: -- * **char** Character-specific data. Every character has its own database. -- * **realm** Realm-specific data. All of the players characters on the same realm share this database. -- * **class** Class-specific data. All of the players characters of the same class share this database. -- * **race** Race-specific data. All of the players characters of the same race share this database. -- * **faction** Faction-specific data. All of the players characters of the same faction share this database. -- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database. -- * **locale** Locale specific data, based on the locale of the players game client. -- * **global** Global Data. All characters on the same account share this database. -- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used. -- -- Creating a new Database using the `:New` function will return a new DBObject. A database will inherit all functions -- of the DBObjectLib listed here. \\ -- If you create a new namespaced child-database (`:RegisterNamespace`), you'll get a DBObject as well, but note -- that the child-databases cannot individually change their profile, and are linked to their parents profile - and because of that, -- the profile related APIs are not available. Only `:RegisterDefaults` and `:ResetProfile` are available on child-databases. -- -- For more details on how to use AceDB-3.0, see the [[AceDB-3.0 Tutorial]]. -- -- You may also be interested in [[libdualspec-1-0|LibDualSpec-1.0]] to do profile switching automatically when switching specs. -- -- @usage -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("DBExample") -- -- -- declare defaults to be used in the DB -- local defaults = { -- profile = { -- setting = true, -- } -- } -- -- function MyAddon:OnInitialize() -- -- Assuming the .toc says ## SavedVariables: MyAddonDB -- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true) -- end -- @class file -- @name AceDB-3.0.lua -- @release $Id: AceDB-3.0.lua 1306 2023-06-23 14:55:09Z nevcairiel $ local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 28 local AceDB = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR) if not AceDB then return end -- No upgrade needed -- Lua APIs local type, pairs, next, error = type, pairs, next, error local setmetatable, rawset, rawget = setmetatable, rawset, rawget -- WoW APIs local _G = _G AceDB.db_registry = AceDB.db_registry or {} AceDB.frame = AceDB.frame or CreateFrame("Frame") local CallbackHandler local CallbackDummy = { Fire = function() end } local DBObjectLib = {} --[[------------------------------------------------------------------------- AceDB Utility Functions ---------------------------------------------------------------------------]] -- Simple shallow copy for copying defaults local function copyTable(src, dest) if type(dest) ~= "table" then dest = {} end if type(src) == "table" then for k,v in pairs(src) do if type(v) == "table" then -- try to index the key first so that the metatable creates the defaults, if set, and use that table v = copyTable(v, dest[k]) end dest[k] = v end end return dest end -- Called to add defaults to a section of the database -- -- When a ["*"] default section is indexed with a new key, a table is returned -- and set in the host table. These tables must be cleaned up by removeDefaults -- in order to ensure we don't write empty default tables. local function copyDefaults(dest, src) -- this happens if some value in the SV overwrites our default value with a non-table --if type(dest) ~= "table" then return end for k, v in pairs(src) do if k == "*" or k == "**" then if type(v) == "table" then -- This is a metatable used for table defaults local mt = { -- This handles the lookup and creation of new subtables __index = function(t,k2) if k2 == nil then return nil end local tbl = {} copyDefaults(tbl, v) rawset(t, k2, tbl) return tbl end, } setmetatable(dest, mt) -- handle already existing tables in the SV for dk, dv in pairs(dest) do if not rawget(src, dk) and type(dv) == "table" then copyDefaults(dv, v) end end else -- Values are not tables, so this is just a simple return local mt = {__index = function(t,k2) return k2~=nil and v or nil end} setmetatable(dest, mt) end elseif type(v) == "table" then if not rawget(dest, k) then rawset(dest, k, {}) end if type(dest[k]) == "table" then copyDefaults(dest[k], v) if src['**'] then copyDefaults(dest[k], src['**']) end end else if rawget(dest, k) == nil then rawset(dest, k, v) end end end end -- Called to remove all defaults in the default table from the database local function removeDefaults(db, defaults, blocker) -- remove all metatables from the db, so we don't accidentally create new sub-tables through them setmetatable(db, nil) -- loop through the defaults and remove their content for k,v in pairs(defaults) do if k == "*" or k == "**" then if type(v) == "table" then -- Loop through all the actual k,v pairs and remove for key, value in pairs(db) do if type(value) == "table" then -- if the key was not explicitly specified in the defaults table, just strip everything from * and ** tables if defaults[key] == nil and (not blocker or blocker[key] == nil) then removeDefaults(value, v) -- if the table is empty afterwards, remove it if next(value) == nil then db[key] = nil end -- if it was specified, only strip ** content, but block values which were set in the key table elseif k == "**" then removeDefaults(value, v, defaults[key]) end end end elseif k == "*" then -- check for non-table default for key, value in pairs(db) do if defaults[key] == nil and v == value then db[key] = nil end end end elseif type(v) == "table" and type(db[k]) == "table" then -- if a blocker was set, dive into it, to allow multi-level defaults removeDefaults(db[k], v, blocker and blocker[k]) if next(db[k]) == nil then db[k] = nil end else -- check if the current value matches the default, and that its not blocked by another defaults table if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then db[k] = nil end end end end -- This is called when a table section is first accessed, to set up the defaults local function initSection(db, section, svstore, key, defaults) local sv = rawget(db, "sv") local tableCreated if not sv[svstore] then sv[svstore] = {} end if not sv[svstore][key] then sv[svstore][key] = {} tableCreated = true end local tbl = sv[svstore][key] if defaults then copyDefaults(tbl, defaults) end rawset(db, section, tbl) return tableCreated, tbl end -- Metatable to handle the dynamic creation of sections and copying of sections. local dbmt = { __index = function(t, section) local keys = rawget(t, "keys") local key = keys[section] if key then local defaultTbl = rawget(t, "defaults") local defaults = defaultTbl and defaultTbl[section] if section == "profile" then local new = initSection(t, section, "profiles", key, defaults) if new then -- Callback: OnNewProfile, database, newProfileKey t.callbacks:Fire("OnNewProfile", t, key) end elseif section == "profiles" then local sv = rawget(t, "sv") if not sv.profiles then sv.profiles = {} end rawset(t, "profiles", sv.profiles) elseif section == "global" then local sv = rawget(t, "sv") if not sv.global then sv.global = {} end if defaults then copyDefaults(sv.global, defaults) end rawset(t, section, sv.global) else initSection(t, section, section, key, defaults) end end return rawget(t, section) end } local function validateDefaults(defaults, keyTbl, offset) if not defaults then return end offset = offset or 0 for k in pairs(defaults) do if not keyTbl[k] or k == "profiles" then error(("Usage: AceDBObject:RegisterDefaults(defaults): '%s' is not a valid datatype."):format(k), 3 + offset) end end end local preserve_keys = { ["callbacks"] = true, ["RegisterCallback"] = true, ["UnregisterCallback"] = true, ["UnregisterAllCallbacks"] = true, ["children"] = true, } local realmKey = GetRealmName() local charKey = UnitName("player") .. " - " .. realmKey local _, classKey = UnitClass("player") local _, raceKey = UnitRace("player") local factionKey = UnitFactionGroup("player") local factionrealmKey = factionKey .. " - " .. realmKey local localeKey = GetLocale():lower() local regionTable = { "US", "KR", "EU", "TW", "CN" } local regionKey = regionTable[GetCurrentRegion()] or GetCurrentRegionName() or "TR" local factionrealmregionKey = factionrealmKey .. " - " .. regionKey -- Actual database initialization function local function initdb(sv, defaults, defaultProfile, olddb, parent) -- Generate the database keys for each section -- map "true" to our "Default" profile if defaultProfile == true then defaultProfile = "Default" end local profileKey if not parent then -- Make a container for profile keys if not sv.profileKeys then sv.profileKeys = {} end -- Try to get the profile selected from the char db profileKey = sv.profileKeys[charKey] or defaultProfile or charKey -- save the selected profile for later sv.profileKeys[charKey] = profileKey else -- Use the profile of the parents DB profileKey = parent.keys.profile or defaultProfile or charKey -- clear the profileKeys in the DB, namespaces don't need to store them sv.profileKeys = nil end -- This table contains keys that enable the dynamic creation -- of each section of the table. The 'global' and 'profiles' -- have a key of true, since they are handled in a special case local keyTbl= { ["char"] = charKey, ["realm"] = realmKey, ["class"] = classKey, ["race"] = raceKey, ["faction"] = factionKey, ["factionrealm"] = factionrealmKey, ["factionrealmregion"] = factionrealmregionKey, ["profile"] = profileKey, ["locale"] = localeKey, ["global"] = true, ["profiles"] = true, } validateDefaults(defaults, keyTbl, 1) -- This allows us to use this function to reset an entire database -- Clear out the old database if olddb then for k,v in pairs(olddb) do if not preserve_keys[k] then olddb[k] = nil end end end -- Give this database the metatable so it initializes dynamically local db = setmetatable(olddb or {}, dbmt) if not rawget(db, "callbacks") then -- try to load CallbackHandler-1.0 if it loaded after our library if not CallbackHandler then CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0", true) end db.callbacks = CallbackHandler and CallbackHandler:New(db) or CallbackDummy end -- Copy methods locally into the database object, to avoid hitting -- the metatable when calling methods if not parent then for name, func in pairs(DBObjectLib) do db[name] = func end else -- hack this one in db.RegisterDefaults = DBObjectLib.RegisterDefaults db.ResetProfile = DBObjectLib.ResetProfile end -- Set some properties in the database object db.profiles = sv.profiles db.keys = keyTbl db.sv = sv --db.sv_name = name db.defaults = defaults db.parent = parent -- store the DB in the registry AceDB.db_registry[db] = true return db end -- handle PLAYER_LOGOUT -- strip all defaults from all databases -- and cleans up empty sections local function logoutHandler(frame, event) if event == "PLAYER_LOGOUT" then for db in pairs(AceDB.db_registry) do db.callbacks:Fire("OnDatabaseShutdown", db) db:RegisterDefaults(nil) -- cleanup sections that are empty without defaults local sv = rawget(db, "sv") for section in pairs(db.keys) do if rawget(sv, section) then -- global is special, all other sections have sub-entrys -- also don't delete empty profiles on main dbs, only on namespaces if section ~= "global" and (section ~= "profiles" or rawget(db, "parent")) then for key in pairs(sv[section]) do if not next(sv[section][key]) then sv[section][key] = nil end end end if not next(sv[section]) then sv[section] = nil end end end end end end AceDB.frame:RegisterEvent("PLAYER_LOGOUT") AceDB.frame:SetScript("OnEvent", logoutHandler) --[[------------------------------------------------------------------------- AceDB Object Method Definitions ---------------------------------------------------------------------------]] --- Sets the defaults table for the given database object by clearing any -- that are currently set, and then setting the new defaults. -- @param defaults A table of defaults for this database function DBObjectLib:RegisterDefaults(defaults) if defaults and type(defaults) ~= "table" then error(("Usage: AceDBObject:RegisterDefaults(defaults): 'defaults' - table or nil expected, got %q."):format(type(defaults)), 2) end validateDefaults(defaults, self.keys) -- Remove any currently set defaults if self.defaults then for section,key in pairs(self.keys) do if self.defaults[section] and rawget(self, section) then removeDefaults(self[section], self.defaults[section]) end end end -- Set the DBObject.defaults table self.defaults = defaults -- Copy in any defaults, only touching those sections already created if defaults then for section,key in pairs(self.keys) do if defaults[section] and rawget(self, section) then copyDefaults(self[section], defaults[section]) end end end end --- Changes the profile of the database and all of it's namespaces to the -- supplied named profile -- @param name The name of the profile to set as the current profile function DBObjectLib:SetProfile(name) if type(name) ~= "string" then error(("Usage: AceDBObject:SetProfile(name): 'name' - string expected, got %q."):format(type(name)), 2) end -- changing to the same profile, dont do anything if name == self.keys.profile then return end local oldProfile = self.profile local defaults = self.defaults and self.defaults.profile -- Callback: OnProfileShutdown, database self.callbacks:Fire("OnProfileShutdown", self) if oldProfile and defaults then -- Remove the defaults from the old profile removeDefaults(oldProfile, defaults) end self.profile = nil self.keys["profile"] = name -- if the storage exists, save the new profile -- this won't exist on namespaces. if self.sv.profileKeys then self.sv.profileKeys[charKey] = name end -- populate to child namespaces if self.children then for _, db in pairs(self.children) do DBObjectLib.SetProfile(db, name) end end -- Callback: OnProfileChanged, database, newProfileKey self.callbacks:Fire("OnProfileChanged", self, name) end --- Returns a table with the names of the existing profiles in the database. -- You can optionally supply a table to re-use for this purpose. -- @param tbl A table to store the profile names in (optional) function DBObjectLib:GetProfiles(tbl) if tbl and type(tbl) ~= "table" then error(("Usage: AceDBObject:GetProfiles(tbl): 'tbl' - table or nil expected, got %q."):format(type(tbl)), 2) end -- Clear the container table if tbl then for k,v in pairs(tbl) do tbl[k] = nil end else tbl = {} end local curProfile = self.keys.profile local i = 0 for profileKey in pairs(self.profiles) do i = i + 1 tbl[i] = profileKey if curProfile and profileKey == curProfile then curProfile = nil end end -- Add the current profile, if it hasn't been created yet if curProfile then i = i + 1 tbl[i] = curProfile end return tbl, i end --- Returns the current profile name used by the database function DBObjectLib:GetCurrentProfile() return self.keys.profile end --- Deletes a named profile. This profile must not be the active profile. -- @param name The name of the profile to be deleted -- @param silent If true, do not raise an error when the profile does not exist function DBObjectLib:DeleteProfile(name, silent) if type(name) ~= "string" then error(("Usage: AceDBObject:DeleteProfile(name): 'name' - string expected, got %q."):format(type(name)), 2) end if self.keys.profile == name then error(("Cannot delete the active profile (%q) in an AceDBObject."):format(name), 2) end if not rawget(self.profiles, name) and not silent then error(("Cannot delete profile %q as it does not exist."):format(name), 2) end self.profiles[name] = nil -- populate to child namespaces if self.children then for _, db in pairs(self.children) do DBObjectLib.DeleteProfile(db, name, true) end end -- switch all characters that use this profile back to the default if self.sv.profileKeys then for key, profile in pairs(self.sv.profileKeys) do if profile == name then self.sv.profileKeys[key] = nil end end end -- Callback: OnProfileDeleted, database, profileKey self.callbacks:Fire("OnProfileDeleted", self, name) end --- Copies a named profile into the current profile, overwriting any conflicting -- settings. -- @param name The name of the profile to be copied into the current profile -- @param silent If true, do not raise an error when the profile does not exist function DBObjectLib:CopyProfile(name, silent) if type(name) ~= "string" then error(("Usage: AceDBObject:CopyProfile(name): 'name' - string expected, got %q."):format(type(name)), 2) end if name == self.keys.profile then error(("Cannot have the same source and destination profiles (%q)."):format(name), 2) end if not rawget(self.profiles, name) and not silent then error(("Cannot copy profile %q as it does not exist."):format(name), 2) end -- Reset the profile before copying DBObjectLib.ResetProfile(self, nil, true) local profile = self.profile local source = self.profiles[name] copyTable(source, profile) -- populate to child namespaces if self.children then for _, db in pairs(self.children) do DBObjectLib.CopyProfile(db, name, true) end end -- Callback: OnProfileCopied, database, sourceProfileKey self.callbacks:Fire("OnProfileCopied", self, name) end --- Resets the current profile to the default values (if specified). -- @param noChildren if set to true, the reset will not be populated to the child namespaces of this DB object -- @param noCallbacks if set to true, won't fire the OnProfileReset callback function DBObjectLib:ResetProfile(noChildren, noCallbacks) local profile = self.profile for k,v in pairs(profile) do profile[k] = nil end local defaults = self.defaults and self.defaults.profile if defaults then copyDefaults(profile, defaults) end -- populate to child namespaces if self.children and not noChildren then for _, db in pairs(self.children) do DBObjectLib.ResetProfile(db, nil, noCallbacks) end end -- Callback: OnProfileReset, database if not noCallbacks then self.callbacks:Fire("OnProfileReset", self) end end --- Resets the entire database, using the string defaultProfile as the new default -- profile. -- @param defaultProfile The profile name to use as the default function DBObjectLib:ResetDB(defaultProfile) if defaultProfile and type(defaultProfile) ~= "string" then error(("Usage: AceDBObject:ResetDB(defaultProfile): 'defaultProfile' - string or nil expected, got %q."):format(type(defaultProfile)), 2) end local sv = self.sv for k,v in pairs(sv) do sv[k] = nil end initdb(sv, self.defaults, defaultProfile, self) -- fix the child namespaces if self.children then if not sv.namespaces then sv.namespaces = {} end for name, db in pairs(self.children) do if not sv.namespaces[name] then sv.namespaces[name] = {} end initdb(sv.namespaces[name], db.defaults, self.keys.profile, db, self) end end -- Callback: OnDatabaseReset, database self.callbacks:Fire("OnDatabaseReset", self) -- Callback: OnProfileChanged, database, profileKey self.callbacks:Fire("OnProfileChanged", self, self.keys["profile"]) return self end --- Creates a new database namespace, directly tied to the database. This -- is a full scale database in it's own rights other than the fact that -- it cannot control its profile individually -- @param name The name of the new namespace -- @param defaults A table of values to use as defaults function DBObjectLib:RegisterNamespace(name, defaults) if type(name) ~= "string" then error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - string expected, got %q."):format(type(name)), 2) end if defaults and type(defaults) ~= "table" then error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'defaults' - table or nil expected, got %q."):format(type(defaults)), 2) end if self.children and self.children[name] then error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - a namespace called %q already exists."):format(name), 2) end local sv = self.sv if not sv.namespaces then sv.namespaces = {} end if not sv.namespaces[name] then sv.namespaces[name] = {} end local newDB = initdb(sv.namespaces[name], defaults, self.keys.profile, nil, self) if not self.children then self.children = {} end self.children[name] = newDB return newDB end --- Returns an already existing namespace from the database object. -- @param name The name of the new namespace -- @param silent if true, the addon is optional, silently return nil if its not found -- @usage -- local namespace = self.db:GetNamespace('namespace') -- @return the namespace object if found function DBObjectLib:GetNamespace(name, silent) if type(name) ~= "string" then error(("Usage: AceDBObject:GetNamespace(name): 'name' - string expected, got %q."):format(type(name)), 2) end if not silent and not (self.children and self.children[name]) then error(("Usage: AceDBObject:GetNamespace(name): 'name' - namespace %q does not exist."):format(name), 2) end if not self.children then self.children = {} end return self.children[name] end --[[------------------------------------------------------------------------- AceDB Exposed Methods ---------------------------------------------------------------------------]] --- Creates a new database object that can be used to handle database settings and profiles. -- By default, an empty DB is created, using a character specific profile. -- -- You can override the default profile used by passing any profile name as the third argument, -- or by passing //true// as the third argument to use a globally shared profile called "Default". -- -- Note that there is no token replacement in the default profile name, passing a defaultProfile as "char" -- will use a profile named "char", and not a character-specific profile. -- @param tbl The name of variable, or table to use for the database -- @param defaults A table of database defaults -- @param defaultProfile The name of the default profile. If not set, a character specific profile will be used as the default. -- You can also pass //true// to use a shared global profile called "Default". -- @usage -- -- Create an empty DB using a character-specific default profile. -- self.db = LibStub("AceDB-3.0"):New("MyAddonDB") -- @usage -- -- Create a DB using defaults and using a shared default profile -- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true) function AceDB:New(tbl, defaults, defaultProfile) if type(tbl) == "string" then local name = tbl tbl = _G[name] if not tbl then tbl = {} _G[name] = tbl end end if type(tbl) ~= "table" then error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'tbl' - table expected, got %q."):format(type(tbl)), 2) end if defaults and type(defaults) ~= "table" then error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaults' - table expected, got %q."):format(type(defaults)), 2) end if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaultProfile' - string or true expected, got %q."):format(type(defaultProfile)), 2) end return initdb(tbl, defaults, defaultProfile) end -- upgrade existing databases for db in pairs(AceDB.db_registry) do if not db.parent then for name,func in pairs(DBObjectLib) do db[name] = func end else db.RegisterDefaults = DBObjectLib.RegisterDefaults db.ResetProfile = DBObjectLib.ResetProfile end end