TourGuide - I'm so excited, I've got Felwood!
[WoW-TourGuide.git] / Dongle.lua
blob0a0ff66c4d94f1815a13f96c4ad4632210890ad9
1 --[[-------------------------------------------------------------------------
2 Copyright (c) 2006-2007, Dongle Development Team
3 All rights reserved.
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are
7 met:
9 * Redistributions of source code must retain the above copyright
10 notice, this list of conditions and the following disclaimer.
11 * Redistributions in binary form must reproduce the above
12 copyright notice, this list of conditions and the following
13 disclaimer in the documentation and/or other materials provided
14 with the distribution.
15 * Neither the name of the Dongle Development Team nor the names of
16 its contributors may be used to endorse or promote products derived
17 from this software without specific prior written permission.
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 ---------------------------------------------------------------------------]]
31 local major = "DongleStub"
32 local minor = tonumber(string.match("$Revision: 313 $", "(%d+)") or 1)
34 local g = getfenv(0)
36 if not g.DongleStub or g.DongleStub:IsNewerVersion(major, minor) then
37 local lib = setmetatable({}, {
38 __call = function(t,k)
39 if type(t.versions) == "table" and t.versions[k] then
40 return t.versions[k].instance
41 else
42 error("Cannot find a library with name '"..tostring(k).."'", 2)
43 end
44 end
47 function lib:IsNewerVersion(major, minor)
48 local versionData = self.versions and self.versions[major]
50 -- If DongleStub versions have differing major version names
51 -- such as DongleStub-Beta0 and DongleStub-1.0-RC2 then a second
52 -- instance will be loaded, with older logic. This code attempts
53 -- to compensate for that by matching the major version against
54 -- "^DongleStub", and handling the version check correctly.
56 if major:match("^DongleStub") then
57 local oldmajor,oldminor = self:GetVersion()
58 if self.versions and self.versions[oldmajor] then
59 return minor > oldminor
60 else
61 return true
62 end
63 end
65 if not versionData then return true end
66 local oldmajor,oldminor = versionData.instance:GetVersion()
67 return minor > oldminor
68 end
70 local function NilCopyTable(src, dest)
71 for k,v in pairs(dest) do dest[k] = nil end
72 for k,v in pairs(src) do dest[k] = v end
73 end
75 function lib:Register(newInstance, activate, deactivate)
76 assert(type(newInstance.GetVersion) == "function",
77 "Attempt to register a library with DongleStub that does not have a 'GetVersion' method.")
79 local major,minor = newInstance:GetVersion()
80 assert(type(major) == "string",
81 "Attempt to register a library with DongleStub that does not have a proper major version.")
82 assert(type(minor) == "number",
83 "Attempt to register a library with DongleStub that does not have a proper minor version.")
85 -- Generate a log of all library registrations
86 if not self.log then self.log = {} end
87 table.insert(self.log, string.format("Register: %s, %s", major, minor))
89 if not self:IsNewerVersion(major, minor) then return false end
90 if not self.versions then self.versions = {} end
92 local versionData = self.versions[major]
93 if not versionData then
94 -- New major version
95 versionData = {
96 ["instance"] = newInstance,
97 ["deactivate"] = deactivate,
100 self.versions[major] = versionData
101 if type(activate) == "function" then
102 table.insert(self.log, string.format("Activate: %s, %s", major, minor))
103 activate(newInstance)
105 return newInstance
108 local oldDeactivate = versionData.deactivate
109 local oldInstance = versionData.instance
111 versionData.deactivate = deactivate
113 local skipCopy
114 if type(activate) == "function" then
115 table.insert(self.log, string.format("Activate: %s, %s", major, minor))
116 skipCopy = activate(newInstance, oldInstance)
119 -- Deactivate the old libary if necessary
120 if type(oldDeactivate) == "function" then
121 local major, minor = oldInstance:GetVersion()
122 table.insert(self.log, string.format("Deactivate: %s, %s", major, minor))
123 oldDeactivate(oldInstance, newInstance)
126 -- Re-use the old table, and discard the new one
127 if not skipCopy then
128 NilCopyTable(newInstance, oldInstance)
130 return oldInstance
133 function lib:GetVersion() return major,minor end
135 local function Activate(new, old)
136 -- This code ensures that we'll move the versions table even
137 -- if the major version names are different, in the case of
138 -- DongleStub
139 if not old then old = g.DongleStub end
141 if old then
142 new.versions = old.versions
143 new.log = old.log
145 g.DongleStub = new
148 -- Actually trigger libary activation here
149 local stub = g.DongleStub or lib
150 lib = stub:Register(lib, Activate)
153 --[[-------------------------------------------------------------------------
154 Begin Library Implementation
155 ---------------------------------------------------------------------------]]
157 local major = "Dongle-1.0"
158 local minor = tonumber(string.match("$Revision: 612 $", "(%d+)") or 1) + 500
159 -- ** IMPORTANT NOTE **
160 -- Due to some issues we had previously with Dongle revision numbers
161 -- we need to artificially inflate the minor revision number, to ensure
162 -- we load sequentially.
164 assert(DongleStub, string.format("%s requires DongleStub.", major))
166 if not DongleStub:IsNewerVersion(major, minor) then return end
168 local Dongle = {}
169 local methods = {
170 "RegisterEvent", "UnregisterEvent", "UnregisterAllEvents", "IsEventRegistered",
171 "RegisterMessage", "UnregisterMessage", "UnregisterAllMessages", "TriggerMessage", "IsMessageRegistered",
172 "EnableDebug", "IsDebugEnabled", "Print", "PrintF", "Debug", "DebugF", "Echo", "EchoF",
173 "InitializeDB",
174 "InitializeSlashCommand",
175 "NewModule", "HasModule", "IterateModules",
178 local registry = {}
179 local lookup = {}
180 local loadqueue = {}
181 local loadorder = {}
182 local events = {}
183 local databases = {}
184 local commands = {}
185 local messages = {}
187 local frame
189 --[[-------------------------------------------------------------------------
190 Message Localization
191 ---------------------------------------------------------------------------]]
193 local L = {
194 ["ADDMESSAGE_REQUIRED"] = "The frame you specify must have an 'AddMessage' method.",
195 ["ALREADY_REGISTERED"] = "A Dongle with the name '%s' is already registered.",
196 ["BAD_ARGUMENT"] = "bad argument #%d to '%s' (%s expected, got %s)",
197 ["BAD_ARGUMENT_DB"] = "bad argument #%d to '%s' (DongleDB expected)",
198 ["CANNOT_DELETE_ACTIVE_PROFILE"] = "You cannot delete your active profile. Change profiles, then attempt to delete.",
199 ["DELETE_NONEXISTANT_PROFILE"] = "You cannot delete a non-existant profile.",
200 ["MUST_CALLFROM_DBOBJECT"] = "You must call '%s' from a Dongle database object.",
201 ["MUST_CALLFROM_REGISTERED"] = "You must call '%s' from a registered Dongle.",
202 ["MUST_CALLFROM_SLASH"] = "You must call '%s' from a Dongle slash command object.",
203 ["PROFILE_DOES_NOT_EXIST"] = "Profile '%s' doesn't exist.",
204 ["REPLACE_DEFAULTS"] = "You are attempting to register defaults with a database that already contains defaults.",
205 ["SAME_SOURCE_DEST"] = "Source/Destination profile cannot be the same profile.",
206 ["EVENT_REGISTER_SPECIAL"] = "You cannot register for the '%s' event. Use the '%s' method instead.",
207 ["Unknown"] = "Unknown",
208 ["INJECTDB_USAGE"] = "Usage: DongleCmd:InjectDBCommands(db, ['copy', 'delete', 'list', 'reset', 'set'])",
209 ["DBSLASH_PROFILE_COPY_DESC"] = "profile copy <name> - Copies profile <name> into your current profile.",
210 ["DBSLASH_PROFILE_COPY_PATTERN"] = "^profile copy (.+)$",
211 ["DBSLASH_PROFILE_DELETE_DESC"] = "profile delete <name> - Deletes the profile <name>.",
212 ["DBSLASH_PROFILE_DELETE_PATTERN"] = "^profile delete (.+)$",
213 ["DBSLASH_PROFILE_LIST_DESC"] = "profile list - Lists all valid profiles.",
214 ["DBSLASH_PROFILE_LIST_PATTERN"] = "^profile list$",
215 ["DBSLASH_PROFILE_RESET_DESC"] = "profile reset - Resets the current profile.",
216 ["DBSLASH_PROFILE_RESET_PATTERN"] = "^profile reset$",
217 ["DBSLASH_PROFILE_SET_DESC"] = "profile set <name> - Sets the current profile to <name>.",
218 ["DBSLASH_PROFILE_SET_PATTERN"] = "^profile set (.+)$",
219 ["DBSLASH_PROFILE_LIST_OUT"] = "Profile List:",
222 --[[-------------------------------------------------------------------------
223 Utility functions for Dongle use
224 ---------------------------------------------------------------------------]]
226 local function assert(level,condition,message)
227 if not condition then
228 error(message,level)
232 local function argcheck(value, num, ...)
233 if type(num) ~= "number" then
234 error(L["BAD_ARGUMENT"]:format(2, "argcheck", "number", type(num)), 1)
237 for i=1,select("#", ...) do
238 if type(value) == select(i, ...) then return end
241 local types = strjoin(", ", ...)
242 local name = string.match(debugstack(2,2,0), ": in function [`<](.-)['>]")
243 error(L["BAD_ARGUMENT"]:format(num, name, types, type(value)), 3)
246 local function safecall(func,...)
247 local success,err = pcall(func,...)
248 if not success then
249 geterrorhandler()(err)
253 --[[-------------------------------------------------------------------------
254 Dongle constructor, and DongleModule system
255 ---------------------------------------------------------------------------]]
257 function Dongle:New(name, obj)
258 argcheck(name, 2, "string")
259 argcheck(obj, 3, "table", "nil")
261 if not obj then
262 obj = {}
265 if registry[name] then
266 error(string.format(L["ALREADY_REGISTERED"], name))
269 local reg = {["obj"] = obj, ["name"] = name}
271 registry[name] = reg
272 lookup[obj] = reg
273 lookup[name] = reg
275 for k,v in pairs(methods) do
276 obj[v] = self[v]
279 -- Add this Dongle to the end of the queue
280 table.insert(loadqueue, obj)
281 return obj,name
284 function Dongle:NewModule(name, obj)
285 local reg = lookup[self]
286 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "NewModule"))
287 argcheck(name, 2, "string")
288 argcheck(obj, 3, "table", "nil")
290 obj,name = Dongle:New(name, obj)
292 if not reg.modules then reg.modules = {} end
293 reg.modules[obj] = obj
294 reg.modules[name] = obj
296 return obj,name
299 function Dongle:HasModule(module)
300 local reg = lookup[self]
301 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "HasModule"))
302 argcheck(module, 2, "string", "table")
304 return reg.modules and reg.modules[module]
307 local function ModuleIterator(t, name)
308 if not t then return end
309 local obj
310 repeat
311 name,obj = next(t, name)
312 until type(name) == "string" or not name
314 return name,obj
317 function Dongle:IterateModules()
318 local reg = lookup[self]
319 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IterateModules"))
321 return ModuleIterator, reg.modules
324 --[[-------------------------------------------------------------------------
325 Event registration system
326 ---------------------------------------------------------------------------]]
328 local function OnEvent(frame, event, ...)
329 local eventTbl = events[event]
330 if eventTbl then
331 for obj,func in pairs(eventTbl) do
332 if type(func) == "string" then
333 if type(obj[func]) == "function" then
334 safecall(obj[func], obj, event, ...)
336 else
337 safecall(func, event, ...)
343 local specialEvents = {
344 ["PLAYER_LOGIN"] = "Enable",
345 ["PLAYER_LOGOUT"] = "Disable",
348 function Dongle:RegisterEvent(event, func)
349 local reg = lookup[self]
350 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "RegisterEvent"))
351 argcheck(event, 2, "string")
352 argcheck(func, 3, "string", "function", "nil")
354 local special = (self ~= Dongle) and specialEvents[event]
355 if special then
356 error(string.format(L["EVENT_REGISTER_SPECIAL"], event, special), 3)
359 -- Name the method the same as the event if necessary
360 if not func then func = event end
362 if not events[event] then
363 events[event] = {}
364 frame:RegisterEvent(event)
366 events[event][self] = func
369 function Dongle:UnregisterEvent(event)
370 local reg = lookup[self]
371 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterEvent"))
372 argcheck(event, 2, "string")
374 local tbl = events[event]
375 if tbl then
376 tbl[self] = nil
377 if not next(tbl) then
378 events[event] = nil
379 frame:UnregisterEvent(event)
384 function Dongle:UnregisterAllEvents()
385 local reg = lookup[self]
386 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterAllEvents"))
388 for event,tbl in pairs(events) do
389 tbl[self] = nil
390 if not next(tbl) then
391 events[event] = nil
392 frame:UnregisterEvent(event)
397 function Dongle:IsEventRegistered(event)
398 local reg = lookup[self]
399 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IsEventRegistered"))
400 argcheck(event, 2, "string")
402 local tbl = events[event]
403 return tbl
406 --[[-------------------------------------------------------------------------
407 Inter-Addon Messaging System
408 ---------------------------------------------------------------------------]]
410 function Dongle:RegisterMessage(msg, func)
411 argcheck(self, 1, "table")
412 argcheck(msg, 2, "string")
413 argcheck(func, 3, "string", "function", "nil")
415 -- Name the method the same as the message if necessary
416 if not func then func = msg end
418 if not messages[msg] then
419 messages[msg] = {}
421 messages[msg][self] = func
424 function Dongle:UnregisterMessage(msg)
425 argcheck(self, 1, "table")
426 argcheck(msg, 2, "string")
428 local tbl = messages[msg]
429 if tbl then
430 tbl[self] = nil
431 if not next(tbl) then
432 messages[msg] = nil
437 function Dongle:UnregisterAllMessages()
438 argcheck(self, 1, "table")
440 for msg,tbl in pairs(messages) do
441 tbl[self] = nil
442 if not next(tbl) then
443 messages[msg] = nil
448 function Dongle:TriggerMessage(msg, ...)
449 argcheck(self, 1, "table")
450 argcheck(msg, 2, "string")
451 local msgTbl = messages[msg]
452 if not msgTbl then return end
454 for obj,func in pairs(msgTbl) do
455 if type(func) == "string" then
456 if type(obj[func]) == "function" then
457 safecall(obj[func], obj, msg, ...)
459 else
460 safecall(func, msg, ...)
465 function Dongle:IsMessageRegistered(msg)
466 argcheck(self, 1, "table")
467 argcheck(msg, 2, "string")
469 local tbl = messages[msg]
470 return tbl[self]
473 --[[-------------------------------------------------------------------------
474 Debug and Print utility functions
475 ---------------------------------------------------------------------------]]
477 function Dongle:EnableDebug(level, frame)
478 local reg = lookup[self]
479 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug"))
480 argcheck(level, 2, "number", "nil")
481 argcheck(frame, 3, "table", "nil")
483 assert(3, type(frame) == "nil" or type(frame.AddMessage) == "function", L["ADDMESSAGE_REQUIRED"])
484 reg.debugFrame = frame or ChatFrame1
485 reg.debugLevel = level
488 function Dongle:IsDebugEnabled()
489 local reg = lookup[self]
490 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug"))
492 return reg.debugLevel, reg.debugFrame
495 local function argsToStrings(a1, ...)
496 if select("#", ...) > 0 then
497 return tostring(a1), argsToStrings(...)
498 else
499 return tostring(a1)
503 local function printHelp(obj, method, header, frame, msg, ...)
504 local reg = lookup[obj]
505 assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method))
507 local name = reg.name
509 if header then
510 msg = "|cFF33FF99"..name.."|r: "..tostring(msg)
513 if select("#", ...) > 0 then
514 msg = string.join(", ", msg, argsToStrings(...))
517 frame:AddMessage(msg)
520 local function printFHelp(obj, method, header, frame, msg, ...)
521 local reg = lookup[obj]
522 assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method))
524 local name = reg.name
525 local success,txt
527 if header then
528 msg = "|cFF33FF99%s|r: " .. msg
529 success,txt = pcall(string.format, msg, name, ...)
530 else
531 success,txt = pcall(string.format, msg, ...)
534 if success then
535 frame:AddMessage(txt)
536 else
537 error(string.gsub(txt, "'%?'", string.format("'%s'", method)), 3)
541 function Dongle:Print(msg, ...)
542 local reg = lookup[self]
543 assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Print"))
544 argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
545 return printHelp(self, "Print", true, DEFAULT_CHAT_FRAME, msg, ...)
548 function Dongle:PrintF(msg, ...)
549 local reg = lookup[self]
550 assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "PrintF"))
551 argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
552 return printFHelp(self, "PrintF", true, DEFAULT_CHAT_FRAME, msg, ...)
555 function Dongle:Echo(msg, ...)
556 local reg = lookup[self]
557 assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Echo"))
558 argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
559 return printHelp(self, "Echo", false, DEFAULT_CHAT_FRAME, msg, ...)
562 function Dongle:EchoF(msg, ...)
563 local reg = lookup[self]
564 assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EchoF"))
565 argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
566 return printFHelp(self, "EchoF", false, DEFAULT_CHAT_FRAME, msg, ...)
569 function Dongle:Debug(level, ...)
570 local reg = lookup[self]
571 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Debug"))
572 argcheck(level, 2, "number")
574 if reg.debugLevel and level <= reg.debugLevel then
575 printHelp(self, "Debug", true, reg.debugFrame, ...)
579 function Dongle:DebugF(level, ...)
580 local reg = lookup[self]
581 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "DebugF"))
582 argcheck(level, 2, "number")
584 if reg.debugLevel and level <= reg.debugLevel then
585 printFHelp(self, "DebugF", true, reg.debugFrame, ...)
589 --[[-------------------------------------------------------------------------
590 Database System
591 ---------------------------------------------------------------------------]]
593 local dbMethods = {
594 "RegisterDefaults", "SetProfile", "GetProfiles", "DeleteProfile", "CopyProfile",
595 "GetCurrentProfile", "ResetProfile", "ResetDB",
596 "RegisterNamespace",
599 local function copyTable(src)
600 local dest = {}
601 for k,v in pairs(src) do
602 if type(k) == "table" then
603 k = copyTable(k)
605 if type(v) == "table" then
606 v = copyTable(v)
608 dest[k] = v
610 return dest
613 local function copyDefaults(dest, src, force)
614 for k,v in pairs(src) do
615 if k == "*" then
616 if type(v) == "table" then
617 -- Values are tables, need some magic here
618 local mt = {
619 __cache = {},
620 __index = function(t,k)
621 local mt = getmetatable(dest)
622 local cache = rawget(mt, "__cache")
623 local tbl = rawget(cache, k)
624 if not tbl then
625 local parent = t
626 local parentkey = k
627 tbl = copyTable(v)
628 rawset(cache, k, tbl)
629 local mt = getmetatable(tbl)
630 if not mt then
631 mt = {}
632 setmetatable(tbl, mt)
634 local newindex = function(t,k,v)
635 rawset(parent, parentkey, t)
636 rawset(t, k, v)
638 rawset(mt, "__newindex", newindex)
640 return tbl
641 end,
643 setmetatable(dest, mt)
644 -- Now need to set the metatable on any child tables
645 for dkey,dval in pairs(dest) do
646 copyDefaults(dval, v)
648 else
649 -- Values are not tables, so this is just a simple return
650 local mt = {__index = function() return v end}
651 setmetatable(dest, mt)
653 elseif type(v) == "table" then
654 if not dest[k] then dest[k] = {} end
655 copyDefaults(dest[k], v, force)
656 else
657 if (dest[k] == nil) or force then
658 dest[k] = v
664 local function removeDefaults(db, defaults)
665 if not db then return end
666 for k,v in pairs(defaults) do
667 if k == "*" and type(v) == "table" then
668 -- check for any defaults that have been changed
669 local mt = getmetatable(db)
670 local cache = rawget(mt, "__cache")
672 for cacheKey,cacheValue in pairs(cache) do
673 removeDefaults(cacheValue, v)
674 if next(cacheValue) ~= nil then
675 -- Something's changed
676 rawset(db, cacheKey, cacheValue)
679 -- Now loop through all the actual k,v pairs and remove
680 for key,value in pairs(db) do
681 removeDefaults(value, v)
683 elseif type(v) == "table" and db[k] then
684 removeDefaults(db[k], v)
685 if not next(db[k]) then
686 db[k] = nil
688 else
689 if db[k] == defaults[k] then
690 db[k] = nil
696 local function initSection(db, section, svstore, key, defaults)
697 local sv = rawget(db, "sv")
699 local tableCreated
700 if not sv[svstore] then sv[svstore] = {} end
701 if not sv[svstore][key] then
702 sv[svstore][key] = {}
703 tableCreated = true
706 local tbl = sv[svstore][key]
708 if defaults then
709 copyDefaults(tbl, defaults)
711 rawset(db, section, tbl)
713 return tableCreated, tbl
716 local dbmt = {
717 __index = function(t, section)
718 local keys = rawget(t, "keys")
719 local key = keys[section]
720 if key then
721 local defaultTbl = rawget(t, "defaults")
722 local defaults = defaultTbl and defaultTbl[section]
724 if section == "profile" then
725 local new = initSection(t, section, "profiles", key, defaults)
726 if new then
727 Dongle:TriggerMessage("DONGLE_PROFILE_CREATED", t, rawget(t, "parent"), rawget(t, "sv_name"), key)
729 elseif section == "profiles" then
730 local sv = rawget(t, "sv")
731 if not sv.profiles then sv.profiles = {} end
732 rawset(t, "profiles", sv.profiles)
733 elseif section == "global" then
734 local sv = rawget(t, "sv")
735 if not sv.global then sv.global = {} end
736 if defaults then
737 copyDefaults(sv.global, defaults)
739 rawset(t, section, sv.global)
740 else
741 initSection(t, section, section, key, defaults)
745 return rawget(t, section)
749 local function initdb(parent, name, defaults, defaultProfile, olddb)
750 -- This allows us to use an arbitrary table as base instead of saved variable name
751 local sv
752 if type(name) == "string" then
753 sv = getglobal(name)
754 if not sv then
755 sv = {}
756 setglobal(name, sv)
758 elseif type(name) == "table" then
759 sv = name
762 -- Generate the database keys for each section
763 local char = string.format("%s - %s", UnitName("player"), GetRealmName())
764 local realm = GetRealmName()
765 local class = select(2, UnitClass("player"))
766 local race = select(2, UnitRace("player"))
767 local faction = UnitFactionGroup("player")
768 local factionrealm = string.format("%s - %s", faction, realm)
770 -- Make a container for profile keys
771 if not sv.profileKeys then sv.profileKeys = {} end
773 -- Try to get the profile selected from the char db
774 local profileKey = sv.profileKeys[char] or defaultProfile or char
775 sv.profileKeys[char] = profileKey
777 local keyTbl= {
778 ["char"] = char,
779 ["realm"] = realm,
780 ["class"] = class,
781 ["race"] = race,
782 ["faction"] = faction,
783 ["factionrealm"] = factionrealm,
784 ["global"] = true,
785 ["profile"] = profileKey,
786 ["profiles"] = true, -- Don't create until we need
789 -- If we've been passed an old database, clear it out
790 if olddb then
791 for k,v in pairs(olddb) do olddb[k] = nil end
794 -- Give this database the metatable so it initializes dynamically
795 local db = setmetatable(olddb or {}, dbmt)
797 -- Copy methods locally
798 for idx,method in pairs(dbMethods) do
799 db[method] = Dongle[method]
802 -- Set some properties in the object we're returning
803 db.profiles = sv.profiles
804 db.keys = keyTbl
805 db.sv = sv
806 db.sv_name = name
807 db.defaults = defaults
808 db.parent = parent
810 databases[db] = true
812 return db
815 function Dongle:InitializeDB(name, defaults, defaultProfile)
816 local reg = lookup[self]
817 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeDB"))
818 argcheck(name, 2, "string", "table")
819 argcheck(defaults, 3, "table", "nil")
820 argcheck(defaultProfile, 4, "string", "nil")
822 return initdb(self, name, defaults, defaultProfile)
825 -- This function operates on a Dongle DB object
826 function Dongle.RegisterDefaults(db, defaults)
827 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterDefaults"))
828 assert(3, db.defaults == nil, L["REPLACE_DEFAUTS"])
829 argcheck(defaults, 2, "table")
831 for section,key in pairs(db.keys) do
832 if defaults[section] and rawget(db, section) then
833 copyDefaults(db[section], defaults[section])
837 db.defaults = defaults
840 function Dongle:ClearDBDefaults()
841 for db in pairs(databases) do
842 local defaults = db.defaults
843 local sv = db.sv
845 if db and defaults then
846 for section,key in pairs(db.keys) do
847 if defaults[section] and rawget(db, section) then
848 removeDefaults(db[section], defaults[section])
852 for section,key in pairs(db.keys) do
853 local tbl = rawget(db, section)
854 if tbl and not next(tbl) then
855 if sv[section] then
856 if type(key) == "string" then
857 sv[section][key] = nil
858 else
859 sv[section] = nil
868 function Dongle.SetProfile(db, name)
869 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "SetProfile"))
870 argcheck(name, 2, "string")
872 local old = db.profile
873 local defaults = db.defaults and db.defaults.profile
875 if defaults then
876 -- Remove the defaults from the old profile
877 removeDefaults(old, defaults)
880 db.profile = nil
881 db.keys["profile"] = name
882 db.sv.profileKeys[db.keys.char] = name
884 Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile)
887 function Dongle.GetProfiles(db, t)
888 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetProfiles"))
889 argcheck(t, 2, "table", "nil")
891 t = t or {}
892 local i = 1
893 for profileKey in pairs(db.sv.profiles) do
894 t[i] = profileKey
895 i = i + 1
897 return t, i - 1
900 function Dongle.GetCurrentProfile(db)
901 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetCurrentProfile"))
902 return db.keys.profile
905 function Dongle.DeleteProfile(db, name)
906 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "DeleteProfile"))
907 argcheck(name, 2, "string")
909 if db.keys.profile == name then
910 error(L["CANNOT_DELETE_ACTIVE_PROFILE"], 2)
913 assert(type(db.sv.profiles[name]) == "table", L["DELETE_NONEXISTANT_PROFILE"])
915 db.sv.profiles[name] = nil
916 Dongle:TriggerMessage("DONGLE_PROFILE_DELETED", db, db.parent, db.sv_name, name)
919 function Dongle.CopyProfile(db, name)
920 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "CopyProfile"))
921 argcheck(name, 2, "string")
923 assert(3, db.keys.profile ~= name, L["SAME_SOURCE_DEST"])
924 assert(3, type(db.sv.profiles[name]) == "table", string.format(L["PROFILE_DOES_NOT_EXIST"], name))
926 local profile = db.profile
927 local source = db.sv.profiles[name]
929 copyDefaults(profile, source, true)
930 Dongle:TriggerMessage("DONGLE_PROFILE_COPIED", db, db.parent, db.sv_name, name, db.keys.profile)
933 function Dongle.ResetProfile(db)
934 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetProfile"))
936 local profile = db.profile
938 for k,v in pairs(profile) do
939 profile[k] = nil
942 local defaults = db.defaults and db.defaults.profile
943 if defaults then
944 copyDefaults(profile, defaults)
946 Dongle:TriggerMessage("DONGLE_PROFILE_RESET", db, db.parent, db.sv_name, db.keys.profile)
950 function Dongle.ResetDB(db, defaultProfile)
951 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetDB"))
952 argcheck(defaultProfile, 2, "nil", "string")
954 local sv = db.sv
955 for k,v in pairs(sv) do
956 sv[k] = nil
959 local parent = db.parent
961 initdb(parent, db.sv_name, db.defaults, defaultProfile, db)
962 Dongle:TriggerMessage("DONGLE_DATABASE_RESET", db, parent, db.sv_name, db.keys.profile)
963 Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile)
964 return db
967 function Dongle.RegisterNamespace(db, name, defaults)
968 assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterNamespace"))
969 argcheck(name, 2, "string")
970 argcheck(defaults, 3, "nil", "string")
972 local sv = db.sv
973 if not sv.namespaces then sv.namespaces = {} end
974 if not sv.namespaces[name] then
975 sv.namespaces[name] = {}
978 local newDB = initdb(db, sv.namespaces[name], defaults, db.keys.profile)
979 -- Remove the :SetProfile method from newDB
980 newDB.SetProfile = nil
982 if not db.children then db.children = {} end
983 table.insert(db.children, newDB)
984 return newDB
987 --[[-------------------------------------------------------------------------
988 Slash Command System
989 ---------------------------------------------------------------------------]]
991 local slashCmdMethods = {
992 "InjectDBCommands",
993 "RegisterSlashHandler",
994 "PrintUsage",
997 local function OnSlashCommand(cmd, cmd_line)
998 if cmd.patterns then
999 for idx,tbl in pairs(cmd.patterns) do
1000 local pattern = tbl.pattern
1001 if string.match(cmd_line, pattern) then
1002 local handler = tbl.handler
1003 if type(tbl.handler) == "string" then
1004 local obj
1005 -- Look in the command object before we look at the parent object
1006 if cmd[handler] then obj = cmd end
1007 if cmd.parent[handler] then obj = cmd.parent end
1008 if obj then
1009 obj[handler](obj, string.match(cmd_line, pattern))
1011 else
1012 handler(string.match(cmd_line, pattern))
1014 return
1018 cmd:PrintUsage()
1021 function Dongle:InitializeSlashCommand(desc, name, ...)
1022 local reg = lookup[self]
1023 assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeSlashCommand"))
1024 argcheck(desc, 2, "string")
1025 argcheck(name, 3, "string")
1026 argcheck(select(1, ...), 4, "string")
1027 for i = 2,select("#", ...) do
1028 argcheck(select(i, ...), i+2, "string")
1031 local cmd = {}
1032 cmd.desc = desc
1033 cmd.name = name
1034 cmd.parent = self
1035 cmd.slashes = { ... }
1036 for idx,method in pairs(slashCmdMethods) do
1037 cmd[method] = Dongle[method]
1040 local genv = getfenv(0)
1042 for i = 1,select("#", ...) do
1043 genv["SLASH_"..name..tostring(i)] = "/"..select(i, ...)
1046 genv.SlashCmdList[name] = function(...) OnSlashCommand(cmd, ...) end
1048 commands[cmd] = true
1050 return cmd
1053 function Dongle.RegisterSlashHandler(cmd, desc, pattern, handler)
1054 assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "RegisterSlashHandler"))
1056 argcheck(desc, 2, "string")
1057 argcheck(pattern, 3, "string")
1058 argcheck(handler, 4, "function", "string")
1060 if not cmd.patterns then
1061 cmd.patterns = {}
1064 table.insert(cmd.patterns, {
1065 ["desc"] = desc,
1066 ["handler"] = handler,
1067 ["pattern"] = pattern,
1071 function Dongle.PrintUsage(cmd)
1072 assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "PrintUsage"))
1073 local parent = cmd.parent
1075 parent:Echo(cmd.desc.."\n".."/"..table.concat(cmd.slashes, ", /")..":\n")
1076 if cmd.patterns then
1077 for idx,tbl in ipairs(cmd.patterns) do
1078 parent:Echo(" - " .. tbl.desc)
1083 local dbcommands = {
1084 ["copy"] = {
1085 L["DBSLASH_PROFILE_COPY_DESC"],
1086 L["DBSLASH_PROFILE_COPY_PATTERN"],
1087 "CopyProfile",
1089 ["delete"] = {
1090 L["DBSLASH_PROFILE_DELETE_DESC"],
1091 L["DBSLASH_PROFILE_DELETE_PATTERN"],
1092 "DeleteProfile",
1094 ["list"] = {
1095 L["DBSLASH_PROFILE_LIST_DESC"],
1096 L["DBSLASH_PROFILE_LIST_PATTERN"],
1098 ["reset"] = {
1099 L["DBSLASH_PROFILE_RESET_DESC"],
1100 L["DBSLASH_PROFILE_RESET_PATTERN"],
1101 "ResetProfile",
1103 ["set"] = {
1104 L["DBSLASH_PROFILE_SET_DESC"],
1105 L["DBSLASH_PROFILE_SET_PATTERN"],
1106 "SetProfile",
1110 function Dongle.InjectDBCommands(cmd, db, ...)
1111 assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "InjectDBCommands"))
1112 assert(3, databases[db], string.format(L["BAD_ARGUMENT_DB"], 2, "InjectDBCommands"))
1113 local argc = select("#", ...)
1114 assert(3, argc > 0, L["INJECTDB_USAGE"])
1116 for i=1,argc do
1117 local cmdname = string.lower(select(i, ...))
1118 local entry = dbcommands[cmdname]
1119 assert(entry, L["INJECTDB_USAGE"])
1120 local func = entry[3]
1122 local handler
1123 if cmdname == "list" then
1124 handler = function(...)
1125 local profiles = db:GetProfiles()
1126 db.parent:Print(L["DBSLASH_PROFILE_LIST_OUT"] .. "\n" .. strjoin("\n", unpack(profiles)))
1128 else
1129 handler = function(...) db[entry[3]](db, ...) end
1132 cmd:RegisterSlashHandler(entry[1], entry[2], handler)
1136 --[[-------------------------------------------------------------------------
1137 Internal Message/Event Handlers
1138 ---------------------------------------------------------------------------]]
1140 local function PLAYER_LOGOUT(event)
1141 Dongle:ClearDBDefaults()
1142 for k,v in pairs(registry) do
1143 local obj = v.obj
1144 if type(obj["Disable"]) == "function" then
1145 safecall(obj["Disable"], obj)
1150 local function PLAYER_LOGIN()
1151 Dongle.initialized = true
1152 for i=1, #loadorder do
1153 local obj = loadorder[i]
1154 if type(obj.Enable) == "function" then
1155 safecall(obj.Enable, obj)
1157 loadorder[i] = nil
1161 local function ADDON_LOADED(event, ...)
1162 for i=1, #loadqueue do
1163 local obj = loadqueue[i]
1164 table.insert(loadorder, obj)
1166 if type(obj.Initialize) == "function" then
1167 safecall(obj.Initialize, obj)
1169 loadqueue[i] = nil
1172 if not Dongle.initialized then
1173 if type(IsLoggedIn) == "function" then
1174 Dongle.initialized = IsLoggedIn()
1175 else
1176 Dongle.initialized = ChatFrame1.defaultLanguage
1180 if Dongle.initialized then
1181 for i=1, #loadorder do
1182 local obj = loadorder[i]
1183 if type(obj.Enable) == "function" then
1184 safecall(obj.Enable, obj)
1186 loadorder[i] = nil
1191 local function DONGLE_PROFILE_CHANGED(msg, db, parent, sv_name, profileKey)
1192 local children = db.children
1193 if children then
1194 for i,namespace in ipairs(children) do
1195 local old = namespace.profile
1196 local defaults = namespace.defaults and namespace.defaults.profile
1198 if defaults then
1199 -- Remove the defaults from the old profile
1200 removeDefaults(old, defaults)
1203 namespace.profile = nil
1204 namespace.keys["profile"] = profileKey
1209 --[[-------------------------------------------------------------------------
1210 DongleStub required functions and registration
1211 ---------------------------------------------------------------------------]]
1213 function Dongle:GetVersion() return major,minor end
1215 local function Activate(self, old)
1216 if old then
1217 registry = old.registry or registry
1218 lookup = old.lookup or lookup
1219 loadqueue = old.loadqueue or loadqueue
1220 loadorder = old.loadorder or loadorder
1221 events = old.events or events
1222 databases = old.databases or databases
1223 commands = old.commands or commands
1224 messages = old.messages or messages
1225 frame = old.frame or CreateFrame("Frame")
1226 else
1227 frame = CreateFrame("Frame")
1228 local reg = {obj = self, name = "Dongle"}
1229 registry[major] = reg
1230 lookup[self] = reg
1231 lookup[major] = reg
1234 self.registry = registry
1235 self.lookup = lookup
1236 self.loadqueue = loadqueue
1237 self.loadorder = loadorder
1238 self.events = events
1239 self.databases = databases
1240 self.commands = commands
1241 self.messages = messages
1242 self.frame = frame
1244 frame:SetScript("OnEvent", OnEvent)
1246 local lib = old or self
1248 -- Lets make sure the lookup table has us.
1249 lookup[lib] = lookup[major]
1251 -- Register for events using Dongle itself
1252 lib:RegisterEvent("ADDON_LOADED", ADDON_LOADED)
1253 lib:RegisterEvent("PLAYER_LOGIN", PLAYER_LOGIN)
1254 lib:RegisterEvent("PLAYER_LOGOUT", PLAYER_LOGOUT)
1255 lib:RegisterMessage("DONGLE_PROFILE_CHANGED", DONGLE_PROFILE_CHANGED)
1257 -- Convert all the modules handles
1258 for name,obj in pairs(registry) do
1259 for k,v in ipairs(methods) do
1260 obj[k] = self[v]
1264 -- Convert all database methods
1265 for db in pairs(databases) do
1266 for idx,method in ipairs(dbMethods) do
1267 db[method] = self[method]
1271 -- Convert all slash command methods
1272 for cmd in pairs(commands) do
1273 for idx,method in ipairs(slashCmdMethods) do
1274 cmd[method] = self[method]
1279 -- Lets nuke any Dongle deactivate functions, please
1280 -- I hate nasty hacks.
1281 if DongleStub.versions and DongleStub.versions[major] then
1282 local reg = DongleStub.versions[major]
1283 reg.deactivate = nil
1286 Dongle = DongleStub:Register(Dongle, Activate)