--[[------------------------------------------------------------------------- Copyright (c) 2006-2007, Dongle Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Dongle Development Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------]] local major = "DongleStub" local minor = tonumber(string.match("$Revision: 313 $", "(%d+)") or 1) local g = getfenv(0) if not g.DongleStub or g.DongleStub:IsNewerVersion(major, minor) then local lib = setmetatable({}, { __call = function(t,k) if type(t.versions) == "table" and t.versions[k] then return t.versions[k].instance else error("Cannot find a library with name '"..tostring(k).."'", 2) end end }) function lib:IsNewerVersion(major, minor) local versionData = self.versions and self.versions[major] -- If DongleStub versions have differing major version names -- such as DongleStub-Beta0 and DongleStub-1.0-RC2 then a second -- instance will be loaded, with older logic. This code attempts -- to compensate for that by matching the major version against -- "^DongleStub", and handling the version check correctly. if major:match("^DongleStub") then local oldmajor,oldminor = self:GetVersion() if self.versions and self.versions[oldmajor] then return minor > oldminor else return true end end if not versionData then return true end local oldmajor,oldminor = versionData.instance:GetVersion() return minor > oldminor end local function NilCopyTable(src, dest) for k,v in pairs(dest) do dest[k] = nil end for k,v in pairs(src) do dest[k] = v end end function lib:Register(newInstance, activate, deactivate) assert(type(newInstance.GetVersion) == "function", "Attempt to register a library with DongleStub that does not have a 'GetVersion' method.") local major,minor = newInstance:GetVersion() assert(type(major) == "string", "Attempt to register a library with DongleStub that does not have a proper major version.") assert(type(minor) == "number", "Attempt to register a library with DongleStub that does not have a proper minor version.") -- Generate a log of all library registrations if not self.log then self.log = {} end table.insert(self.log, string.format("Register: %s, %s", major, minor)) if not self:IsNewerVersion(major, minor) then return false end if not self.versions then self.versions = {} end local versionData = self.versions[major] if not versionData then -- New major version versionData = { ["instance"] = newInstance, ["deactivate"] = deactivate, } self.versions[major] = versionData if type(activate) == "function" then table.insert(self.log, string.format("Activate: %s, %s", major, minor)) activate(newInstance) end return newInstance end local oldDeactivate = versionData.deactivate local oldInstance = versionData.instance versionData.deactivate = deactivate local skipCopy if type(activate) == "function" then table.insert(self.log, string.format("Activate: %s, %s", major, minor)) skipCopy = activate(newInstance, oldInstance) end -- Deactivate the old libary if necessary if type(oldDeactivate) == "function" then local major, minor = oldInstance:GetVersion() table.insert(self.log, string.format("Deactivate: %s, %s", major, minor)) oldDeactivate(oldInstance, newInstance) end -- Re-use the old table, and discard the new one if not skipCopy then NilCopyTable(newInstance, oldInstance) end return oldInstance end function lib:GetVersion() return major,minor end local function Activate(new, old) -- This code ensures that we'll move the versions table even -- if the major version names are different, in the case of -- DongleStub if not old then old = g.DongleStub end if old then new.versions = old.versions new.log = old.log end g.DongleStub = new end -- Actually trigger libary activation here local stub = g.DongleStub or lib lib = stub:Register(lib, Activate) end --[[------------------------------------------------------------------------- Begin Library Implementation ---------------------------------------------------------------------------]] local major = "Dongle-1.1" local minor = tonumber(string.match("$Revision: 647 $", "(%d+)") or 1) assert(DongleStub, string.format("%s requires DongleStub.", major)) if not DongleStub:IsNewerVersion(major, minor) then return end local Dongle = {} local methods = { "RegisterEvent", "UnregisterEvent", "UnregisterAllEvents", "IsEventRegistered", "RegisterMessage", "UnregisterMessage", "UnregisterAllMessages", "TriggerMessage", "IsMessageRegistered", "ScheduleTimer", "ScheduleRepeatingTimer", "CancelTimer", "IsTimerScheduled", "EnableDebug", "IsDebugEnabled", "Print", "PrintF", "Debug", "DebugF", "Echo", "EchoF", "InitializeDB", "InitializeSlashCommand", "NewModule", "HasModule", "IterateModules", } local registry = {} local lookup = {} local loadqueue = {} local loadorder = {} local events = {} local databases = {} local commands = {} local messages = {} local timers = {} local heap = {} local frame --[[------------------------------------------------------------------------- Message Localization ---------------------------------------------------------------------------]] local L = { ["ADDMESSAGE_REQUIRED"] = "The frame you specify must have an 'AddMessage' method.", ["ALREADY_REGISTERED"] = "A Dongle with the name '%s' is already registered.", ["BAD_ARGUMENT"] = "bad argument #%d to '%s' (%s expected, got %s)", ["BAD_ARGUMENT_DB"] = "bad argument #%d to '%s' (DongleDB expected)", ["CANNOT_DELETE_ACTIVE_PROFILE"] = "You cannot delete your active profile. Change profiles, then attempt to delete.", ["DELETE_NONEXISTANT_PROFILE"] = "You cannot delete a non-existant profile.", ["MUST_CALLFROM_DBOBJECT"] = "You must call '%s' from a Dongle database object.", ["MUST_CALLFROM_REGISTERED"] = "You must call '%s' from a registered Dongle.", ["MUST_CALLFROM_SLASH"] = "You must call '%s' from a Dongle slash command object.", ["PROFILE_DOES_NOT_EXIST"] = "Profile '%s' doesn't exist.", ["REPLACE_DEFAULTS"] = "You are attempting to register defaults with a database that already contains defaults.", ["SAME_SOURCE_DEST"] = "Source/Destination profile cannot be the same profile.", ["EVENT_REGISTER_SPECIAL"] = "You cannot register for the '%s' event. Use the '%s' method instead.", ["Unknown"] = "Unknown", ["INJECTDB_USAGE"] = "Usage: DongleCmd:InjectDBCommands(db, ['copy', 'delete', 'list', 'reset', 'set'])", ["DBSLASH_PROFILE_COPY_DESC"] = "profile copy <name> - Copies profile <name> into your current profile.", ["DBSLASH_PROFILE_COPY_PATTERN"] = "^profile copy (.+)$", ["DBSLASH_PROFILE_DELETE_DESC"] = "profile delete <name> - Deletes the profile <name>.", ["DBSLASH_PROFILE_DELETE_PATTERN"] = "^profile delete (.+)$", ["DBSLASH_PROFILE_LIST_DESC"] = "profile list - Lists all valid profiles.", ["DBSLASH_PROFILE_LIST_PATTERN"] = "^profile list$", ["DBSLASH_PROFILE_RESET_DESC"] = "profile reset - Resets the current profile.", ["DBSLASH_PROFILE_RESET_PATTERN"] = "^profile reset$", ["DBSLASH_PROFILE_SET_DESC"] = "profile set <name> - Sets the current profile to <name>.", ["DBSLASH_PROFILE_SET_PATTERN"] = "^profile set (.+)$", ["DBSLASH_PROFILE_LIST_OUT"] = "Profile List:", } --[[------------------------------------------------------------------------- Utility functions for Dongle use ---------------------------------------------------------------------------]] local function assert(level,condition,message) if not condition then error(message,level) end end local function argcheck(value, num, ...) if type(num) ~= "number" then error(L["BAD_ARGUMENT"]:format(2, "argcheck", "number", type(num)), 1) end for i=1,select("#", ...) do if type(value) == select(i, ...) then return end end local types = strjoin(", ", ...) local name = string.match(debugstack(2,2,0), ": in function [`<](.-)['>]") error(L["BAD_ARGUMENT"]:format(num, name, types, type(value)), 3) end local function safecall(func,...) local success,err = pcall(func,...) if not success then geterrorhandler()(err) end end --[[------------------------------------------------------------------------- Dongle constructor, and DongleModule system ---------------------------------------------------------------------------]] function Dongle:New(name, obj) argcheck(name, 2, "string") argcheck(obj, 3, "table", "nil") if not obj then obj = {} end if registry[name] then error(string.format(L["ALREADY_REGISTERED"], name)) end local reg = {["obj"] = obj, ["name"] = name} registry[name] = reg lookup[obj] = reg lookup[name] = reg for k,v in pairs(methods) do obj[v] = self[v] end -- Add this Dongle to the end of the queue table.insert(loadqueue, obj) return obj,name end function Dongle:NewModule(name, obj) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "NewModule")) argcheck(name, 2, "string") argcheck(obj, 3, "table", "nil") obj,name = Dongle:New(name, obj) if not reg.modules then reg.modules = {} end reg.modules[obj] = obj reg.modules[name] = obj return obj,name end function Dongle:HasModule(module) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "HasModule")) argcheck(module, 2, "string", "table") return reg.modules and reg.modules[module] end local function ModuleIterator(t, name) if not t then return end local obj repeat name,obj = next(t, name) until type(name) == "string" or not name return name,obj end function Dongle:IterateModules() local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IterateModules")) return ModuleIterator, reg.modules end --[[------------------------------------------------------------------------- Event registration system ---------------------------------------------------------------------------]] local function OnEvent(frame, event, ...) local eventTbl = events[event] if eventTbl then for obj,func in pairs(eventTbl) do if type(func) == "string" then if type(obj[func]) == "function" then safecall(obj[func], obj, event, ...) end else safecall(func, event, ...) end end end end local specialEvents = { ["PLAYER_LOGIN"] = "Enable", ["PLAYER_LOGOUT"] = "Disable", } function Dongle:RegisterEvent(event, func) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "RegisterEvent")) argcheck(event, 2, "string") argcheck(func, 3, "string", "function", "nil") local special = (self ~= Dongle) and specialEvents[event] if special then error(string.format(L["EVENT_REGISTER_SPECIAL"], event, special), 3) end -- Name the method the same as the event if necessary if not func then func = event end if not events[event] then events[event] = {} frame:RegisterEvent(event) end events[event][self] = func end function Dongle:UnregisterEvent(event) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterEvent")) argcheck(event, 2, "string") local tbl = events[event] if tbl then tbl[self] = nil if not next(tbl) then events[event] = nil frame:UnregisterEvent(event) end end end function Dongle:UnregisterAllEvents() local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterAllEvents")) for event,tbl in pairs(events) do tbl[self] = nil if not next(tbl) then events[event] = nil frame:UnregisterEvent(event) end end end function Dongle:IsEventRegistered(event) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IsEventRegistered")) argcheck(event, 2, "string") local tbl = events[event] return tbl end --[[------------------------------------------------------------------------- Inter-Addon Messaging System ---------------------------------------------------------------------------]] function Dongle:RegisterMessage(msg, func) argcheck(self, 1, "table") argcheck(msg, 2, "string") argcheck(func, 3, "string", "function", "nil") -- Name the method the same as the message if necessary if not func then func = msg end if not messages[msg] then messages[msg] = {} end messages[msg][self] = func end function Dongle:UnregisterMessage(msg) argcheck(self, 1, "table") argcheck(msg, 2, "string") local tbl = messages[msg] if tbl then tbl[self] = nil if not next(tbl) then messages[msg] = nil end end end function Dongle:UnregisterAllMessages() argcheck(self, 1, "table") for msg,tbl in pairs(messages) do tbl[self] = nil if not next(tbl) then messages[msg] = nil end end end function Dongle:TriggerMessage(msg, ...) argcheck(self, 1, "table") argcheck(msg, 2, "string") local msgTbl = messages[msg] if not msgTbl then return end for obj,func in pairs(msgTbl) do if type(func) == "string" then if type(obj[func]) == "function" then safecall(obj[func], obj, msg, ...) end else safecall(func, msg, ...) end end end function Dongle:IsMessageRegistered(msg) argcheck(self, 1, "table") argcheck(msg, 2, "string") local tbl = messages[msg] return tbl[self] end --[[------------------------------------------------------------------------- Timer System ---------------------------------------------------------------------------]] local function HeapSwap(i1, i2) heap[i1], heap[i2] = heap[i2], heap[i1] end local function HeapBubbleUp(index) while index > 1 do local parentIndex = math.floor(index / 2) if heap[index].timeToFire < heap[parentIndex].timeToFire then HeapSwap(index, parentIndex) index = parentIndex else break end end end local function HeapBubbleDown(index) while 2 * index <= heap.lastIndex do local leftIndex = 2 * index local rightIndex = leftIndex + 1 local current = heap[index] local leftChild = heap[leftIndex] local rightChild = heap[rightIndex] if not rightChild then if leftChild.timeToFire < current.timeToFire then HeapSwap(index, leftIndex) index = leftIndex else break end else if leftChild.timeToFire < current.timeToFire or rightChild.timeToFire < current.timeToFire then if leftChild.timeToFire < rightChild.timeToFire then HeapSwap(index, leftIndex) index = leftIndex else HeapSwap(index, rightIndex) index = rightIndex end else break end end end end local function OnUpdate(frame, elapsed) local schedule = heap[1] while schedule and schedule.timeToFire < GetTime() do if schedule.cancelled then HeapSwap(1, heap.lastIndex) heap[heap.lastIndex] = nil heap.lastIndex = heap.lastIndex - 1 HeapBubbleDown(1) else if schedule.args then safecall(schedule.func, schedule.name, unpack(schedule.args)) else safecall(schedule.func, schedule.name) end if schedule.repeating then schedule.timeToFire = schedule.timeToFire + schedule.repeating HeapBubbleDown(1) else HeapSwap(1, heap.lastIndex) heap[heap.lastIndex] = nil heap.lastIndex = heap.lastIndex - 1 HeapBubbleDown(1) timers[schedule.name] = nil end end schedule = heap[1] end if not schedule then frame:Hide() end end function Dongle:ScheduleTimer(name, func, delay, ...) argcheck(self, 1, "table") argcheck(name, 2, "string") argcheck(func, 3, "function") argcheck(delay, 4, "number") if Dongle:IsTimerScheduled(name) then Dongle:CancelTimer(name) end local schedule = {} timers[name] = schedule schedule.timeToFire = GetTime() + delay schedule.func = func schedule.name = name if select('#', ...) ~= 0 then schedule.args = { ... } end if heap.lastIndex then heap.lastIndex = heap.lastIndex + 1 else heap.lastIndex = 1 end heap[heap.lastIndex] = schedule HeapBubbleUp(heap.lastIndex) if not frame:IsShown() then frame:Show() end end function Dongle:ScheduleRepeatingTimer(name, func, delay, ...) Dongle:ScheduleTimer(name, func, delay, ...) timers[name].repeating = delay end function Dongle:IsTimerScheduled(name) argcheck(self, 1, "table") argcheck(name, 2, "string") local schedule = timers[name] if schedule then return true, schedule.timeToFire - GetTime() else return false end end function Dongle:CancelTimer(name) argcheck(self, 1, "table") argcheck(name, 2, "string") local schedule = timers[name] if not schedule then return end schedule.cancelled = true timers[name] = nil end --[[------------------------------------------------------------------------- Debug and Print utility functions ---------------------------------------------------------------------------]] function Dongle:EnableDebug(level, frame) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug")) argcheck(level, 2, "number", "nil") argcheck(frame, 3, "table", "nil") assert(3, type(frame) == "nil" or type(frame.AddMessage) == "function", L["ADDMESSAGE_REQUIRED"]) reg.debugFrame = frame or ChatFrame1 reg.debugLevel = level end function Dongle:IsDebugEnabled() local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug")) return reg.debugLevel, reg.debugFrame end local function argsToStrings(a1, ...) if select("#", ...) > 0 then return tostring(a1), argsToStrings(...) else return tostring(a1) end end local function printHelp(obj, method, header, frame, msg, ...) local reg = lookup[obj] assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method)) local name = reg.name if header then msg = "|cFF33FF99"..name.."|r: "..tostring(msg) end if select("#", ...) > 0 then msg = string.join(", ", msg, argsToStrings(...)) end frame:AddMessage(msg) end local function printFHelp(obj, method, header, frame, msg, ...) local reg = lookup[obj] assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method)) local name = reg.name local success,txt if header then msg = "|cFF33FF99%s|r: " .. msg success,txt = pcall(string.format, msg, name, ...) else success,txt = pcall(string.format, msg, ...) end if success then frame:AddMessage(txt) else error(string.gsub(txt, "'%?'", string.format("'%s'", method)), 3) end end function Dongle:Print(msg, ...) local reg = lookup[self] assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Print")) argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata") return printHelp(self, "Print", true, DEFAULT_CHAT_FRAME, msg, ...) end function Dongle:PrintF(msg, ...) local reg = lookup[self] assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "PrintF")) argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata") return printFHelp(self, "PrintF", true, DEFAULT_CHAT_FRAME, msg, ...) end function Dongle:Echo(msg, ...) local reg = lookup[self] assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Echo")) argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata") return printHelp(self, "Echo", false, DEFAULT_CHAT_FRAME, msg, ...) end function Dongle:EchoF(msg, ...) local reg = lookup[self] assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EchoF")) argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata") return printFHelp(self, "EchoF", false, DEFAULT_CHAT_FRAME, msg, ...) end function Dongle:Debug(level, ...) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Debug")) argcheck(level, 2, "number") if reg.debugLevel and level <= reg.debugLevel then printHelp(self, "Debug", true, reg.debugFrame, ...) end end function Dongle:DebugF(level, ...) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "DebugF")) argcheck(level, 2, "number") if reg.debugLevel and level <= reg.debugLevel then printFHelp(self, "DebugF", true, reg.debugFrame, ...) end end --[[------------------------------------------------------------------------- Database System ---------------------------------------------------------------------------]] local dbMethods = { "RegisterDefaults", "SetProfile", "GetProfiles", "DeleteProfile", "CopyProfile", "GetCurrentProfile", "ResetProfile", "ResetDB", "RegisterNamespace", } local function copyTable(src) local dest = {} for k,v in pairs(src) do if type(k) == "table" then k = copyTable(k) end if type(v) == "table" then v = copyTable(v) end dest[k] = v end return dest end local function copyDefaults(dest, src, force) for k,v in pairs(src) do if k == "*" then if type(v) == "table" then -- Values are tables, need some magic here local mt = { __cache = {}, __index = function(t,k) local mt = getmetatable(dest) local cache = rawget(mt, "__cache") local tbl = rawget(cache, k) if not tbl then local parent = t local parentkey = k tbl = copyTable(v) rawset(cache, k, tbl) local mt = getmetatable(tbl) if not mt then mt = {} setmetatable(tbl, mt) end local newindex = function(t,k,v) rawset(parent, parentkey, t) rawset(t, k, v) end rawset(mt, "__newindex", newindex) end return tbl end, } setmetatable(dest, mt) -- Now need to set the metatable on any child tables for dkey,dval in pairs(dest) do copyDefaults(dval, v) end else -- Values are not tables, so this is just a simple return local mt = {__index = function() return v end} setmetatable(dest, mt) end elseif type(v) == "table" then if not dest[k] then dest[k] = {} end copyDefaults(dest[k], v, force) else if (dest[k] == nil) or force then dest[k] = v end end end end local function removeDefaults(db, defaults) if not db then return end for k,v in pairs(defaults) do if k == "*" and type(v) == "table" then -- check for any defaults that have been changed local mt = getmetatable(db) local cache = rawget(mt, "__cache") for cacheKey,cacheValue in pairs(cache) do removeDefaults(cacheValue, v) if next(cacheValue) ~= nil then -- Something's changed rawset(db, cacheKey, cacheValue) end end -- Now loop through all the actual k,v pairs and remove for key,value in pairs(db) do removeDefaults(value, v) end elseif type(v) == "table" and db[k] then removeDefaults(db[k], v) if not next(db[k]) then db[k] = nil end else if db[k] == defaults[k] then db[k] = nil end end end end 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 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 Dongle:TriggerMessage("DONGLE_PROFILE_CREATED", t, rawget(t, "parent"), rawget(t, "sv_name"), 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 initdb(parent, name, defaults, defaultProfile, olddb) -- This allows us to use an arbitrary table as base instead of saved variable name local sv if type(name) == "string" then sv = getglobal(name) if not sv then sv = {} setglobal(name, sv) end elseif type(name) == "table" then sv = name end -- Generate the database keys for each section local char = string.format("%s - %s", UnitName("player"), GetRealmName()) local realm = GetRealmName() local class = select(2, UnitClass("player")) local race = select(2, UnitRace("player")) local faction = UnitFactionGroup("player") local factionrealm = string.format("%s - %s", faction, realm) -- Make a container for profile keys if not sv.profileKeys then sv.profileKeys = {} end -- Try to get the profile selected from the char db local profileKey = sv.profileKeys[char] or defaultProfile or char sv.profileKeys[char] = profileKey local keyTbl= { ["char"] = char, ["realm"] = realm, ["class"] = class, ["race"] = race, ["faction"] = faction, ["factionrealm"] = factionrealm, ["global"] = true, ["profile"] = profileKey, ["profiles"] = true, -- Don't create until we need } -- If we've been passed an old database, clear it out if olddb then for k,v in pairs(olddb) do olddb[k] = nil end end -- Give this database the metatable so it initializes dynamically local db = setmetatable(olddb or {}, dbmt) -- Copy methods locally for idx,method in pairs(dbMethods) do db[method] = Dongle[method] end -- Set some properties in the object we're returning db.profiles = sv.profiles db.keys = keyTbl db.sv = sv db.sv_name = name db.defaults = defaults db.parent = parent databases[db] = true return db end function Dongle:InitializeDB(name, defaults, defaultProfile) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeDB")) argcheck(name, 2, "string", "table") argcheck(defaults, 3, "table", "nil") argcheck(defaultProfile, 4, "string", "nil") return initdb(self, name, defaults, defaultProfile) end -- This function operates on a Dongle DB object function Dongle.RegisterDefaults(db, defaults) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterDefaults")) assert(3, db.defaults == nil, L["REPLACE_DEFAUTS"]) argcheck(defaults, 2, "table") for section,key in pairs(db.keys) do if defaults[section] and rawget(db, section) then copyDefaults(db[section], defaults[section]) end end db.defaults = defaults end function Dongle:ClearDBDefaults() for db in pairs(databases) do local defaults = db.defaults local sv = db.sv if db and defaults then for section,key in pairs(db.keys) do if defaults[section] and rawget(db, section) then removeDefaults(db[section], defaults[section]) end end for section,key in pairs(db.keys) do local tbl = rawget(db, section) if tbl and not next(tbl) then if sv[section] then if type(key) == "string" then sv[section][key] = nil else sv[section] = nil end end end end end end end function Dongle.SetProfile(db, name) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "SetProfile")) argcheck(name, 2, "string") local old = db.profile local defaults = db.defaults and db.defaults.profile if defaults then -- Remove the defaults from the old profile removeDefaults(old, defaults) end db.profile = nil db.keys["profile"] = name db.sv.profileKeys[db.keys.char] = name Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile) end function Dongle.GetProfiles(db, tbl) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetProfiles")) argcheck(t, 2, "table", "nil") -- Clear the container table if tbl then for k,v in pairs(tbl) do tbl[k] = nil end else tbl = {} end local i = 0 for profileKey in pairs(db.profiles) do i = i + 1 tbl[i] = profileKey end -- Add the current profile, if it hasn't been created yet if rawget(db, "profile") == nil then i = i + 1 tbl[i] = db.keys.profile end return tbl, i end function Dongle.GetCurrentProfile(db) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetCurrentProfile")) return db.keys.profile end function Dongle.DeleteProfile(db, name) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "DeleteProfile")) argcheck(name, 2, "string") if db.keys.profile == name then error(L["CANNOT_DELETE_ACTIVE_PROFILE"], 2) end assert(type(db.sv.profiles[name]) == "table", L["DELETE_NONEXISTANT_PROFILE"]) db.sv.profiles[name] = nil Dongle:TriggerMessage("DONGLE_PROFILE_DELETED", db, db.parent, db.sv_name, name) end function Dongle.CopyProfile(db, name) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "CopyProfile")) argcheck(name, 2, "string") assert(3, db.keys.profile ~= name, L["SAME_SOURCE_DEST"]) assert(3, type(db.sv.profiles[name]) == "table", string.format(L["PROFILE_DOES_NOT_EXIST"], name)) local profile = db.profile local source = db.sv.profiles[name] copyDefaults(profile, source, true) Dongle:TriggerMessage("DONGLE_PROFILE_COPIED", db, db.parent, db.sv_name, name, db.keys.profile) end function Dongle.ResetProfile(db) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetProfile")) local profile = db.profile for k,v in pairs(profile) do profile[k] = nil end local defaults = db.defaults and db.defaults.profile if defaults then copyDefaults(profile, defaults) end Dongle:TriggerMessage("DONGLE_PROFILE_RESET", db, db.parent, db.sv_name, db.keys.profile) end function Dongle.ResetDB(db, defaultProfile) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetDB")) argcheck(defaultProfile, 2, "nil", "string") local sv = db.sv for k,v in pairs(sv) do sv[k] = nil end local parent = db.parent initdb(parent, db.sv_name, db.defaults, defaultProfile, db) Dongle:TriggerMessage("DONGLE_DATABASE_RESET", db, parent, db.sv_name, db.keys.profile) Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile) return db end function Dongle.RegisterNamespace(db, name, defaults) assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterNamespace")) argcheck(name, 2, "string") argcheck(defaults, 3, "nil", "table") local sv = db.sv if not sv.namespaces then sv.namespaces = {} end if not sv.namespaces[name] then sv.namespaces[name] = {} end local newDB = initdb(db, sv.namespaces[name], defaults, db.keys.profile) -- Remove the :SetProfile method from newDB newDB.SetProfile = nil if not db.children then db.children = {} end table.insert(db.children, newDB) return newDB end --[[------------------------------------------------------------------------- Slash Command System ---------------------------------------------------------------------------]] local slashCmdMethods = { "InjectDBCommands", "RegisterSlashHandler", "PrintUsage", } local function OnSlashCommand(cmd, cmd_line) if cmd.patterns then for idx,tbl in pairs(cmd.patterns) do local pattern = tbl.pattern if string.match(cmd_line, pattern) then local handler = tbl.handler if type(tbl.handler) == "string" then local obj -- Look in the command object before we look at the parent object if cmd[handler] then obj = cmd end if cmd.parent[handler] then obj = cmd.parent end if obj then obj[handler](obj, string.match(cmd_line, pattern)) end else handler(string.match(cmd_line, pattern)) end return end end end cmd:PrintUsage() end function Dongle:InitializeSlashCommand(desc, name, ...) local reg = lookup[self] assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeSlashCommand")) argcheck(desc, 2, "string") argcheck(name, 3, "string") argcheck(select(1, ...), 4, "string") for i = 2,select("#", ...) do argcheck(select(i, ...), i+2, "string") end local cmd = {} cmd.desc = desc cmd.name = name cmd.parent = self cmd.slashes = { ... } for idx,method in pairs(slashCmdMethods) do cmd[method] = Dongle[method] end local genv = getfenv(0) for i = 1,select("#", ...) do genv["SLASH_"..name..tostring(i)] = "/"..select(i, ...) end genv.SlashCmdList[name] = function(...) OnSlashCommand(cmd, ...) end commands[cmd] = true return cmd end function Dongle.RegisterSlashHandler(cmd, desc, pattern, handler) assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "RegisterSlashHandler")) argcheck(desc, 2, "string") argcheck(pattern, 3, "string") argcheck(handler, 4, "function", "string") if not cmd.patterns then cmd.patterns = {} end table.insert(cmd.patterns, { ["desc"] = desc, ["handler"] = handler, ["pattern"] = pattern, }) end function Dongle.PrintUsage(cmd) assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "PrintUsage")) local parent = cmd.parent parent:Echo(cmd.desc.."\n".."/"..table.concat(cmd.slashes, ", /")..":\n") if cmd.patterns then for idx,tbl in ipairs(cmd.patterns) do parent:Echo(" - " .. tbl.desc) end end end local dbcommands = { ["copy"] = { L["DBSLASH_PROFILE_COPY_DESC"], L["DBSLASH_PROFILE_COPY_PATTERN"], "CopyProfile", }, ["delete"] = { L["DBSLASH_PROFILE_DELETE_DESC"], L["DBSLASH_PROFILE_DELETE_PATTERN"], "DeleteProfile", }, ["list"] = { L["DBSLASH_PROFILE_LIST_DESC"], L["DBSLASH_PROFILE_LIST_PATTERN"], }, ["reset"] = { L["DBSLASH_PROFILE_RESET_DESC"], L["DBSLASH_PROFILE_RESET_PATTERN"], "ResetProfile", }, ["set"] = { L["DBSLASH_PROFILE_SET_DESC"], L["DBSLASH_PROFILE_SET_PATTERN"], "SetProfile", }, } function Dongle.InjectDBCommands(cmd, db, ...) assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "InjectDBCommands")) assert(3, databases[db], string.format(L["BAD_ARGUMENT_DB"], 2, "InjectDBCommands")) local argc = select("#", ...) assert(3, argc > 0, L["INJECTDB_USAGE"]) for i=1,argc do local cmdname = string.lower(select(i, ...)) local entry = dbcommands[cmdname] assert(entry, L["INJECTDB_USAGE"]) local func = entry[3] local handler if cmdname == "list" then handler = function(...) local profiles = db:GetProfiles() db.parent:Print(L["DBSLASH_PROFILE_LIST_OUT"] .. "\n" .. strjoin("\n", unpack(profiles))) end else handler = function(...) db[entry[3]](db, ...) end end cmd:RegisterSlashHandler(entry[1], entry[2], handler) end end --[[------------------------------------------------------------------------- Internal Message/Event Handlers ---------------------------------------------------------------------------]] local function PLAYER_LOGOUT(event) Dongle:ClearDBDefaults() for k,v in pairs(registry) do local obj = v.obj if type(obj["Disable"]) == "function" then safecall(obj["Disable"], obj) end end end local PLAYER_LOGIN do local lockPlayerLogin = false function PLAYER_LOGIN() if lockPlayerLogin then return end lockPlayerLogin = true local obj = table.remove(loadorder, 1) while obj do if type(obj.Enable) == "function" then safecall(obj.Enable, obj) end obj = table.remove(loadorder, 1) end lockPlayerLogin = false end end local function ADDON_LOADED(event, ...) local obj = table.remove(loadqueue, 1) while obj do table.insert(loadorder, obj) if type(obj.Initialize) == "function" then safecall(obj.Initialize, obj) end obj = table.remove(loadqueue, 1) end if IsLoggedIn() then PLAYER_LOGIN() end end local function DONGLE_PROFILE_CHANGED(msg, db, parent, sv_name, profileKey) local children = db.children if children then for i,namespace in ipairs(children) do local old = namespace.profile local defaults = namespace.defaults and namespace.defaults.profile if defaults then -- Remove the defaults from the old profile removeDefaults(old, defaults) end namespace.profile = nil namespace.keys["profile"] = profileKey end end end --[[------------------------------------------------------------------------- DongleStub required functions and registration ---------------------------------------------------------------------------]] function Dongle:GetVersion() return major,minor end local function Activate(self, old) if old then registry = old.registry or registry lookup = old.lookup or lookup loadqueue = old.loadqueue or loadqueue loadorder = old.loadorder or loadorder events = old.events or events databases = old.databases or databases commands = old.commands or commands messages = old.messages or messages frame = old.frame or CreateFrame("Frame") timers = old.timers or timers heap = old.heap or heap else frame = CreateFrame("Frame") local reg = {obj = self, name = "Dongle"} registry[major] = reg lookup[self] = reg lookup[major] = reg end self.registry = registry self.lookup = lookup self.loadqueue = loadqueue self.loadorder = loadorder self.events = events self.databases = databases self.commands = commands self.messages = messages self.frame = frame self.timers = timers self.heap = heap frame:SetScript("OnEvent", OnEvent) frame:SetScript("OnUpdate", OnUpdate) -- Lets ensure the lookup table has our entry -- This fixes an issue with upgrades lookup[self] = lookup[major] -- Register for events using Dongle itself self:RegisterEvent("ADDON_LOADED", ADDON_LOADED) self:RegisterEvent("PLAYER_LOGIN", PLAYER_LOGIN) self:RegisterEvent("PLAYER_LOGOUT", PLAYER_LOGOUT) self:RegisterMessage("DONGLE_PROFILE_CHANGED", DONGLE_PROFILE_CHANGED) -- Convert all the modules handles for name,obj in pairs(registry) do for k,v in ipairs(methods) do obj[k] = self[v] end end -- Convert all database methods for db in pairs(databases) do for idx,method in ipairs(dbMethods) do db[method] = self[method] end end -- Convert all slash command methods for cmd in pairs(commands) do for idx,method in ipairs(slashCmdMethods) do cmd[method] = self[method] end end end Dongle = DongleStub:Register(Dongle, Activate)