--[[ Stubby AddOn for World of Watcraft (tm) Version: 5.9.4961 (WhackyWallaby) Revision: $Id: Stubby.lua 275 2010-10-03 14:00:39Z kandoko $ URL: http://auctioneeraddon.com/dl/Stubby/ Stubby is an addon that allows you to register boot code for your addon. This bootcode will be run whenever your addon does not demand load on startup so that you can setup your own conditions for loading. A quick example of this is: ------------------------------------------- Stubby.RegisterBootCode("myAddOn", "CommandHandler", [=[ local function cmdHandler(msg) LoadAddOn("myAddOn") MyAddOn_Command(msg) end SLASH_MYADDON1 = "/myaddon" SlashCmdList['MYADDON'] = cmdHandler ]=]) ------------------------------------------- So, what did this just do? It registered some boot code (called "CommandHandler") with Stubby that Stubby will (in the case you are not demand loaded) execute on your behalf. In the above example, your boot code sets up a command handler which causes your addon to load and process the command. Another example: ------------------------------------------- Stubby.CreateAddOnLoadBootCode("myAddOn", "Blizzard_AuctionUI") ------------------------------------------- Ok, what was that? Well you just setup some boot code for your addon that will register an addon hook when Stubby loads and your addon doesn't. This addon hook will cause your addon to load when the AuctionUI does. The primary functions that you will be interested in are: CreateAddOnLoadBootCode(ownerAddOn, triggerAddOn) CreateEventLoadBootCode(ownerAddOn, triggerEvent) CreateFunctionLoadBootCode(ownerAddOn, triggerFunction) And the manual, but vastly more powerful: RegisterBootCode(ownerAddOn, bootName, bootCode) Stubby can also save variables for you if you wish to retain stateful information in your boot code. (maybe you have recieved notification from your user that they wish always to have your addon load for the current toon?) These are the variable functions: SetConfig(ownerAddOn, variable, value, isGlobal) GetConfig(ownerAddOn, variable) ClearConfig(ownerAddOn, variable) The SetConfig function sets the configuration variable "variable" for ownerAddOn to value. The variable is per-toon unless isGlobal is set. The GetConfig function gets "variable" for ownerAddOn it will return per-toon values before global ones. The ClearConfig function clears the toon specific and global "variable" for ownerAddOn. The following functions are also available for you to use if you need to use some manual boot code and want to hook into some function, addon or event within your boot code: Stubby.RegisterFunctionHook(triggerFunction, position, hookFunction, ...) Stubby.RegisterAddOnHook(triggerAddOn, ownerAddOn, hookFunction, ...) Stubby.RegisterEventHook(triggerEvent, ownerAddOn, hookFunction, ...) RegisterFunctionHook allows you to hook into a function. * The triggerFunction is a string that names the function you want to hook into. eg: "GameTooltip.SetOwner" * The position is a negative or positive number that defines the actual calling order of the addon. The smaller or more negative the number, the earlier in the call sequence your hookFunction will be called, the larger the number, the later your hook will be called. The actual original (hooked) function is called at position 0, so if your addon is hooked at a negative position, you will not have access to any return values. * You pass (by reference) your function that you wish called as hookFunction. This function will be called with the following parameters: hookFunction(hookParams, returnValue, hook1, hook2 .. hookN) - hookParams is a table containing the additional parameters passed to the RegisterFunctionHook function (the "..." params) - returnValue is an array of the returned values of the function or nil if none. - hook1..hookN are the original parameters of the hooked function in the original order. The passed function can return one of the following three special commands: - "abort" will abort the function call imidiatly and any hook positioned after the function won't be called (this includes the original function, if position of the function retunring "abort" is < 0) - "killorig" causes the original function tonot being called. All other hooked functions will still be called. If position of the functions returning "killorig" is > 0, this has no affect (since the original function was already called before) - "setreturn" sets the return value according to the second return value which must be a table. All function hooks which are called after the function hook which set the return value, will receive this new return value as their second parameter. Note that the original function still resets the return value. So in case you want the complete function hook to return something different than the original function's return value, make sure to set the position > 0. Also note, that any hooked function called after this one can change the return value again. Examples: return "setreturn", {1} sets the return value to 1 return "setreturn", {} sets the return value to nil return "setreturn", {{[1] = "foo", [2] = "bar"}} sets the return value to the table: {[1] = "foo", [2] = "bar"} RegisterAddOnHook is very much like the register function hook call except that there is no positioning (you may get notified in any order with respect to any other addons which may be hooked) * The triggerAddOn specifies the name of the addon of which you want to be notified of it's loading. * The ownerAddOn is your addon's name (used for removing hooks) * The hookFunction is a function that gets called when the triggerAddOn loads or if it is already loaded straight away. This function will be called with the following parameters hookFunction(hookParams) - hookParams is a table containing the additional parameters passed to the RegisterAddOnHook function (the "..." params) RegisterEventHook allows you to hook an event in much the same way as the above functions. * The triggerEvent is an event which causes your hookFunction to be executed. * The ownerAddOn is your addon's name (used for removing hooks) * The hookFunction is a function that gets called whenever the triggerEvent fires (until canceled with UnregisterEventHook) This function will be called with the following parameters: hookFunction(hookParams, event, hook1, hook2 .. hookN) - hookParams is a table containing the additional parameters passed to the RegisterEventHook function (the "..." params) - event is the event string that has just been fired - hook1..hookN are the original parameters of the event function in the original order. Other functions which may be of interest are: UnregisterFunctionHook(triggerFunction, hookFunc) UnregisterAddOnHook(triggerAddOn, ownerAddOn) UnregisterEventHook(triggerEvent, ownerAddOn) UnregisterBootCode(ownerAddOn, bootName) There is also a single exposed 'constant' allowing you to do some basic version checking for compatibility: Stubby.VERSION (introduced in revision 507) This constant is Stubby's revision number, a simple positive integer that will increase by an arbitrary amount with each new version of Stubby. Current $Revision: 275 $ Example: ------------------------------------------- if (Stubby.VERSION and Stubby.VERSION >= 507) then -- Register boot code else Stubby.Print("You need to update your version of Stubby!") end ------------------------------------------- License: This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program(see GPL.txt); if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Note: This AddOn's source code is specifically designed to work with World of Warcraft's interpreted AddOn system. You have an implicit license to use this AddOn with these facilities since that is its designated purpose as per: http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat ]] LibStub("LibRevision"):Set("$URL: http://svn.norganna.org/libs/trunk/Stubby/Stubby.lua $","$Rev: 275 $","5.1.DEV.", 'auctioneer', 'libs') ------------------------------------------------------------------------------- -- Error codes ------------------------------------------------------------------------------- -- 0 = no error / function call succeeded -- 1 = Trying to remove the hooked function from the trigger function failed -- as the hooked function is not hooked in the trigger function at all. -- 2 = Trying to remove the hook from a function failed, as the specified is not -- hooked at all. -- 3 = Trying to remove the hook from a function failed, as another function -- meanwhile hooked into the same function. -- 4 = Invalid function call. One or more parameters are missing or invalid. -- 5 = Failed to compiling a hook. local cleanList local config = { hooks = { functions={}, origFuncs={} }, calls = { functions={}, callList={} }, loads = {}, events = {}, } local DebugLib = LibStub("DebugLib") local debug, assert if DebugLib then debug, assert = DebugLib("Stubby") else function debug() end assert = debug end StubbyConfig = {} -- temporary table used in tableRemoveNilSafe() to modify another table local tempTable = {} -- Function prototypes local chatPrint -- chatPrint(...) local checkAddOns -- checkAddOns() local clearConfig -- clearConfig(ownerAddOn, variable) local createAddOnLoadBootCode -- createAddOnLoadBootCode(ownerAddOn, triggerAddOn) local createEventLoadBootCode -- createEventLoadBootCode(ownerAddOn, triggerEvent) local createFunctionLoadBootCode -- createFunctionLoadBootCode(ownerAddOn, triggerFunction) local errorHandler -- errorHandler(stackLevel, ...) local eventWatcher -- eventWatcher(event) local events -- events(event, param) local getConfig -- getConfig(ownerAddOn, variable) local getOrigFunc -- getOrigFunc(triggerFunction) local getRevision -- getRevision() local hookCall -- hookCall(funcName, ...) local hookInto -- hookInto(triggerFunction) local inspectAddOn -- inspectAddOn(addonName, title, info) local loadWatcher -- loadWatcher(loadedAddOn) local onLoaded -- onLoaded() local onWorldStart -- onWorldStart() local rebuildNotifications -- rebuildNotifications(notifyItems) local registerAddOnHook -- registerAddOnHook(triggerAddOn, ownerAddOn, hookFunction, ...) local registerBootCode -- registerBootCode(ownerAddOn, bootName, bootCode) local registerEventHook -- registerEventHook(triggerEvent, ownerAddOn, hookFunction, ...) local registerFunctionHook -- registerFunctionHook(triggerFunction, position, hookFunc, ...) local runBootCodes -- runBootCodes() local searchForNewAddOns -- searchForNewAddOns() local cleanUpAddOnData -- cleanUpAddOnData() local cleanUpAddOnConfigs -- cleanUpAddOnConfigs() local setConfig -- setConfig(ownerAddOn, variable, value, isGlobal) local shouldInspectAddOn -- shouldInspectAddOn(addonName) local unhookFrom -- unhookFrom(triggerFunction) local unregisterAddOnHook -- unregisterAddOnHook(triggerAddOn, ownerAddOn) local unregisterBootCode -- unregisterBootCode(ownerAddOn, bootName) local unregisterEventHook -- unregisterEventHook(triggerEvent, ownerAddOn) local unregisterFunctionHook -- unregisterFunctionHook(triggerFunction, hookFunc) local tableRemoveNilSafe -- tableRemoveNilSafe(table, [pos]) -- Function definitions -- This function takes all the items and their requested orders -- and assigns an actual ordering to them. function rebuildNotifications(notifyItems) local notifyFuncs = {} for hookType, hData in pairs(notifyItems) do notifyFuncs[hookType] = {} -- Sort all hooks for this type in ascending numerical order. local sortedPositions = {} for requestedPos in pairs(hData) do table.insert(sortedPositions, requestedPos) end table.sort(sortedPositions) -- Process the sorted request list and insert in correct -- order into the call list. for _,requestedPos in ipairs(sortedPositions) do local func = hData[requestedPos] table.insert(notifyFuncs[hookType], func) end end return notifyFuncs end local callDetail = {} local function callDebugger(...) local msg = tostring(...) for i = 2, select("#", ...) do msg = msg.." "..tostring(select(i, ...)) end if (Swatter and Swatter.IsEnabled()) then return Swatter.OnError("Error while calling hook:\n{{{Hook name:}}}\n "..tostring(callDetail[1]).."\n"..msg.."\n{{{Instantiated from:}}}\n "..callDetail[2].n, StubbyHook, debugstack(2, 20, 20)) else return Stubby.Print("Error while calling hook for: "..tostring(callDetail[1])..". "..msg, "\nCall Chain:\n", debugstack(2, 3, 6)) end end local function callRunner(...) local funcName, func, retVal, callParams = unpack(callDetail) local callParamsLen = callParams[1] if (funcName) then return func.f(func.a, retVal, unpack(callParams, 2, callParamsLen+1)) end end -- This function's purpose is to execute all the attached -- functions in order and the original call at just before -- position 0. function hookCall(funcName, ...) local orig = Stubby.GetOrigFunc(funcName) if (not orig) then return end local res local retVal local callParams = { select("#",...), ... } local callees if config.calls and config.calls.callList and config.calls.callList[funcName] then callees = config.calls.callList[funcName] end if (callees) then for _, func in ipairs(callees) do if (orig and func.p >= 0) then retVal = {pcall(orig, ...)} -- After pcall, the first element in the table is either true, if -- pcall succeeded, or false, if it failed. -- So we remove this result code and check if the call was -- successful. If it wasn't, an errormessage is printed. -- We also have to use a nil-safe variation of table.remove() since -- there are blizard functions which return tables containing -- holes, which are not processed as required by table.remove(). if (not tableRemoveNilSafe(retVal, 1)) then local ErrorString = "Error: Original call failed while running hooks: "..tostring(funcName) for key, value in pairs(retVal) do ErrorString = ErrorString.."\n"..tostring(key).." : "..tostring(value) end Stubby.ErrorHandler(2, ErrorString) end orig = nil end callDetail[1] = funcName callDetail[2] = func callDetail[3] = retVal callDetail[4] = callParams local result, res, addit = xpcall(callRunner, callDebugger) if (result) then if (res == 'abort') then return elseif (res == 'killorig') then orig = nil elseif (res == 'setreturn') then retVal = addit returns = true end end end end if (orig) then retVal = {pcall(orig, ...)} if (not tableRemoveNilSafe(retVal, 1)) then Stubby.ErrorHandler(2, "Error: Original call failed after running hooks for: ", tostring(funcName), "\n", retVal[1]) end end if (retVal) then return unpack(retVal, 1, table.maxn(retVal)) end end ------------------------------------------------------------------------------- -- This function automatically hooks Stubby in place of the -- original function, dynamically. -- -- returns: -- first value: -- 0 - if hooking into the triggerFunction was successful -- >0 - errorcode -- second value: -- nil - if hooking into the triggerFunction was successful -- (string) - errormessage -- -- remarks: -- Refere to the Error codes section to get a list of possible errors. ------------------------------------------------------------------------------- Stubby_OldFunction = nil Stubby_NewFunction = nil function hookInto(triggerFunction) assert(triggerFunction, "No trigger function specified when calling hookInto!") if config.hooks.origFuncs[triggerFunction] then -- Stubby is already hooked into this function. No need to do it again. return 0 end local stringToLoad = [[ Stubby_OldFunction = ]]..triggerFunction..[[; local functionString = ']]..triggerFunction..[['; if (not (type(Stubby_OldFunction) == "function")) then return Stubby.ErrorHandler(3, "Error occured while compiling hook: ", tostring(functionString), "is not a valid function") end Stubby_NewFunction = function(...) return Stubby.HookCall(functionString, ...); end; ]]..triggerFunction..[[ = Stubby_NewFunction ]]; local loadedFunction, errorMessage = loadstring(stringToLoad, "StubbyHookingFunction") if (loadedFunction) then loadedFunction() else Stubby_NewFunction = nil Stubby_OldFunction = nil Stubby.ErrorHandler(2, "Error occured while compiling hook:", tostring(triggerFunction), "\n", errorMessage) return 5, "Error occured while compiling hook for "..triggerFunction..". Errormessage: "..errorMessage end config.hooks.functions[triggerFunction] = Stubby_NewFunction config.hooks.origFuncs[triggerFunction] = Stubby_OldFunction Stubby_NewFunction = nil Stubby_OldFunction = nil return 0 end ------------------------------------------------------------------------------- -- Unhooks stubby's hooked function from the given trigger function. -- -- calls: -- getglobal() - if there is a hooked function present -- -- called by: -- TODO -- -- paramaeters: -- triggerFunction - (string) the name of the function to be unhooked -- -- returns: -- first value: -- 0 - if unhooking was successful -- >0 - errorcode -- second value: -- nil - if unhooking was successful -- (string) - errormessage -- -- remarks: -- Refere to the Error codes section to get a list of possible errors. ------------------------------------------------------------------------------- function unhookFrom(triggerFunction) -- check, if the trigger function is really hooked if not config.hooks.origFuncs[triggerFunction] then return 2, "Failed to unhook the trigger function: "..triggerFunction.." since it is not hooked at all." end -- make sure, that no other addon hooked this function meanwhile if _G[triggerFunction] == config.hooks.origFuncs[triggerFunction] then triggerFunction = config.hooks.origFuncs[triggerFunction] config.hooks.origFuncs[triggerFunction] = nil config.hooks.functions[triggerFunction] = nil return 0 end return 3, "Could not unhook the trigger function "..triggerFunction..", since another addon hooked it meanwhile." end function errorHandler(stackLevel, ...) local msg = tostring(...) for i = 2, select("#", ...) do msg = msg.." "..tostring(select(i, ...)) end stackLevel = (stackLevel or 1) + 1 if (Swatter and Swatter.IsEnabled()) then return Swatter.OnError(msg, Stubby, debugstack(stackLevel, 20, 20)) else return Stubby.Print(msg, "\nCall Chain:\n", debugstack(2, 3, 6)) end end function getOrigFunc(triggerFunction) if (config.hooks) and (config.hooks.origFuncs) then return config.hooks.origFuncs[triggerFunction] end end ------------------------------------------------------------------------------- --[[ This function causes a given function to be hooked by stubby and configures the hook function to be called at the given position. The original function gets executed a position 0. Use a negative number to get called before the original function, and positive number to get called after the original function. Default position is 200. If someone else is already using your number, you will get automatically moved up for after or down for before. Please also leave space for other people who may need to position their hooks in between your hook and the original. ]] -- returns: -- first value: -- 0 - if registering the function hook was successful -- >0 - errorcode -- second value: -- nil - if registering the function hook was successful -- (string) - errormessage -- -- remarks: -- Refere to the Error codes section to get a list of possible errors. ------------------------------------------------------------------------------- function registerFunctionHook(triggerFunction, position, hookFunc, ...) if (not (triggerFunction and hookFunc)) then return debug("Invalid function call. No trigger function and/or hook function specified. Usage Stubby.RegisterFunctionHook(triggerFunction, position, hookFunction,...).", 4, DebugLib.Level.Error) end local insertPos = tonumber(position) or 200 local funcObj local hookFuncName = strsplit("\n", debugstack(2,1,0), 2) if (select("#", ...) == 0) then funcObj = { f = hookFunc, n = hookFuncName, p = position, } else funcObj = { f = hookFunc, n = hookFuncName, a = {...}, p = position } end if (not config.calls) then config.calls = {} end if (not config.calls.functions) then config.calls.functions = {} end if (config.calls.functions[triggerFunction]) then while (config.calls.functions[triggerFunction][insertPos]) do if (position >= 0) then insertPos = insertPos + 1 else insertPos = insertPos - 1 end end config.calls.functions[triggerFunction][insertPos] = funcObj else config.calls.functions[triggerFunction] = {} config.calls.functions[triggerFunction][insertPos] = funcObj end config.calls.callList = rebuildNotifications(config.calls.functions) local iErrorCode, strErrorMessage = hookInto(triggerFunction) if iErrorCode > 0 then return debug(strErrorMessage, iErrorCode, DebugLib.Level.Error) else return 0 end end ------------------------------------------------------------------------------- -- Unregisters the hooked function. If the hooked function is hooked multiple -- times, all these hooks are removed. -- -- calls: -- unhookFrom() - if unregistering the last hooked function -- rebuildNotification() - always -- -- called by: -- TODO -- -- paramaeters: -- TODO -- -- returns: -- first value: -- >0 - number of how many times the given hooked function has been -- unhooked -- 0 - indicates an error -- second value: -- 0 - if unhooking was successful -- >0 - errorcode -- third value: -- nil - if unhooking was successful -- (string) - errormessage -- -- remarks: -- Refere to the Error codes section to get a list of possible errors. ------------------------------------------------------------------------------- function unregisterFunctionHook(triggerFunction, hookFunc) if not (config.calls and config.calls.functions and config.calls.functions[triggerFunction]) then return 0, debug("Failed to unregister function hook for "..triggerFunction.." since it is not hooked at all", 1, DebugLib.Level.Error) end local iHooked = 0 local iRemoved = 0 for pos, funcObj in pairs(config.calls.functions[triggerFunction]) do iHooked = iHooked + 1 if (funcObj and funcObj.f == hookFunc) then config.calls.functions[triggerFunction][pos] = nil iRemoved = iRemoved + 1 end end -- config.calls.functions[triggerFunction] should never be empty. If there -- are no hooked functions, it should be nil! -- Otherwise this could produce incorrect error messages. assert(iHooked > 0, "config.calls.functions["..triggerFunction.."] is an empty array!") -- clean up the hooking tables, if no more hooked functions are present if(iHooked == iRemoved) then config.calls.functions[triggerFunction] = nil -- make sure that unhooking was sucessful, or could not be done due to another addon hooking the trigger function meanwhile -- TODO: add something like if canBeUnhooked(triggerFunction) then to disable false error messages assert(unhookFrom(triggerFunction) ~= 2, "unhookFrom() reports the trigger function: "..triggerFunction.." not to be hooked, although it should be!") end if iRemoved == 0 then return 0, debug("Failed to unregister function hook for "..triggerFunction..". The given function is not hooked in this trigger function.", 1, DebugLib.Level.Error) end -- rebuild the call list, so that the removed functions are also removed from -- the call list config.calls.callList = rebuildNotifications(config.calls.functions) return iRemoved, 0 end --[[ This function registers a given function to be called when a given addon is loaded, or immediatly if it is already loaded (this can be used to setup a hooking function to execute when an addon is loaded but not before) In certain cenarios IsAddOnLoaded returns 1 even though addon is not fully loaded yet. See http://jira.norganna.org/browse/STUB-8 for details. In these cases the hook function will be called twice. It should check by querting a global variable form the addon if the addon was actually loaded, before accessing its functionality ]] function registerAddOnHook(triggerAddOn, ownerAddOn, hookFunction, ...) if (IsAddOnLoaded(triggerAddOn)) then if (select("#", ...) == 0) then hookFunction() else hookFunction({...}) end end local addon = triggerAddOn:lower() if (not config.loads[addon]) then config.loads[addon] = {} end config.loads[addon][ownerAddOn] = nil if (hookFunction) then if (select("#", ...) == 0) then config.loads[addon][ownerAddOn] = { f = hookFunction, } else config.loads[addon][ownerAddOn] = { f = hookFunction, a = {...}, } end end end function unregisterAddOnHook(triggerAddOn, ownerAddOn) local addon = triggerAddOn:lower() if (config.loads and config.loads[addon] and config.loads[addon][ownerAddOn]) then config.loads[addon][ownerAddOn] = nil end end function loadWatcher(loadedAddOn) local addon = loadedAddOn:lower() if (config.loads[addon]) then for ownerAddOn, hookDetail in pairs(config.loads[addon]) do hookDetail.f(hookDetail.a) end end end -- This function registers a given function to be called when a given -- event is fired (this can be used to activate an addon upon receipt -- of a given event etc) function registerEventHook(triggerEvent, ownerAddOn, hookFunction, ...) if (not config.events[triggerEvent]) then config.events[triggerEvent] = {} StubbyFrame:RegisterEvent(triggerEvent) end config.events[triggerEvent][ownerAddOn] = nil if (hookFunction) then if (select("#", ...) == 0) then config.events[triggerEvent][ownerAddOn] = { f = hookFunction, } else config.events[triggerEvent][ownerAddOn] = { f = hookFunction, a = {...}, } end end end function unregisterEventHook(triggerEvent, ownerAddOn) if (config.events and config.events[triggerEvent] and config.events[triggerEvent][ownerAddOn]) then config.events[triggerEvent][ownerAddOn] = nil -- events is indexed using the addons name as the key value, -- so we have to use next() to check, if the table is empty -- Debugged by ccox and Cera if ( not next( config.events[triggerEvent] ) ) then config.events[triggerEvent] = nil -- Never unregister ADDON_LOADED, because this is used by RegisterAddOnHook() if (triggerEvent ~= "ADDON_LOADED") then StubbyFrame:UnregisterEvent(triggerEvent) end end end end function eventWatcher(event, ...) if (config.events[event]) then for ownerAddOn, hookDetail in pairs(config.events[event]) do hookDetail.f(hookDetail.a, event, ...) end end end -- This function registers boot code. This is a piece of code -- specified as a string, which Stubby will execute on your behalf -- when we are first loaded. This code can do anything a normal -- lua script can, such as create global functions, register a -- command handler, hook into functions, load your addon etc. -- Leaving bootCode nil will remove your boot. function registerBootCode(ownerAddOn, bootName, bootCode) local ownerIndex = ownerAddOn:lower() local bootIndex = bootName:lower() if (not StubbyConfig.boots) then StubbyConfig.boots = {} end if (not StubbyConfig.boots[ownerIndex]) then StubbyConfig.boots[ownerIndex] = {} end StubbyConfig.boots[ownerIndex][bootIndex] = nil if (bootCode) then StubbyConfig.boots[ownerIndex][bootIndex] = bootCode end end function unregisterBootCode(ownerAddOn, bootName) local ownerIndex = ownerAddOn:lower() local bootIndex = bootName:lower() if not (StubbyConfig.boots) then return end if not (ownerIndex and StubbyConfig.boots[ownerIndex]) then return end if (bootIndex == nil) then StubbyConfig.boots[ownerIndex] = nil else StubbyConfig.boots[ownerIndex][bootIndex] = nil end end function createAddOnLoadBootCode(ownerAddOn, triggerAddOn) registerBootCode(ownerAddOn, triggerAddOn.."AddOnLoader", 'local function hookFunction() '.. 'LoadAddOn("'..ownerAddOn..'") '.. 'Stubby.UnregisterAddOnHook("'..triggerAddOn..'", "'..ownerAddOn..'") '.. 'end '.. 'Stubby.RegisterAddOnHook("'..triggerAddOn..'", "'..ownerAddOn..'", hookFunction)' ) end function createFunctionLoadBootCode(ownerAddOn, triggerFunction) registerBootCode(ownerAddOn, triggerFunction.."FunctionLoader", 'local function hookFunction() '.. 'LoadAddOn("'..ownerAddOn..'") '.. 'Stubby.UnregisterFunctionHook("'..triggerFunction..'", hookFunction) '.. 'end '.. 'Stubby.RegisterFunctionHook("'..triggerFunction..'", 200, hookFunction)' ) end function createEventLoadBootCode(ownerAddOn, triggerEvent) registerBootCode(ownerAddOn, triggerEvent.."FunctionLoader", 'local function hookFunction() '.. 'LoadAddOn("'..ownerAddOn..'") '.. 'Stubby.UnregisterEventHook("'..triggerEvent..'", "'..ownerAddOn..'") '.. 'end '.. 'Stubby.RegisterEventHook("'..triggerEvent..'", "'..ownerAddOn..'", hookFunction)' ) end -- Functions to check through all addons for dependants. -- If any exist that we don't know about, and have a dependancy of us, then we will load them -- once to give them a chance to register themselves with us. function checkAddOns() if not StubbyConfig.inspected then return end local goodList = {} local addonCount = GetNumAddOns() local name, title, notes for i=1, addonCount do name, title, notes = GetAddOnInfo(i) if (StubbyConfig.inspected and StubbyConfig.inspected[name]) then local infoCompare = title.."|"..(notes or "") if (infoCompare == StubbyConfig.addinfo[name]) then goodList[name] = true end end end for name in pairs(StubbyConfig.inspected) do if (not goodList[name]) then StubbyConfig.inspected[name] = nil StubbyConfig.addinfo[name] = nil end end end -- Cleans up boot codes for removed addons and prompts for deletion of their -- configurations. function cleanUpAddOnData() if (not StubbyConfig.boots) then return end for b in pairs(StubbyConfig.boots) do local _,title = GetAddOnInfo(b) if (not title) then StubbyConfig.boots[b] = nil if (StubbyConfig.configs) then if (cleanList == nil) then cleanList = {} end table.insert(cleanList, b) end end end if (cleanList) then cleanUpAddOnConfigs() end end -- Shows confirmation dialogs to clean configuration for addons that have -- just been removed. Warning: Calls itself recursively until done. function cleanUpAddOnConfigs() if (not cleanList) then return end local addonIndex = #cleanList local addonName = cleanList[addonIndex] if (addonIndex == 1) then cleanList = nil else table.remove(cleanList, addonIndex) end StaticPopupDialogs["CLEANUP_STUBBY" .. addonIndex] = { text = "The AddOn \"" .. addonName .. "\" is no longer available. Do you wish to delete it's loading preferences?", button1 = "Delete", button2 = "Keep", OnAccept = function() StubbyConfig.configs[addonName] = nil cleanUpAddOnConfigs() end, OnCancel = function() cleanUpAddOnConfigs() end, timeout = 0, whileDead = 1, } StaticPopup_Show("CLEANUP_STUBBY" .. addonIndex, "","") end function shouldInspectAddOn(addonName) if not StubbyConfig.inspected[addonName] then return true end return false end function inspectAddOn(addonName, title, info) LoadAddOn(addonName) StubbyConfig.inspected[addonName] = true StubbyConfig.addinfo[addonName] = title.."|"..(info or "") end function searchForNewAddOns() local addonCount = GetNumAddOns() local name, title, notes, enabled, loadable, reason, security, requiresLoad for i=1, addonCount do requiresLoad = false name, title, notes, enabled, loadable, reason, security = GetAddOnInfo(i) if (IsAddOnLoadOnDemand(i) and shouldInspectAddOn(name) and loadable) then local addonDeps = { GetAddOnDependencies(i) } for _, dependancy in pairs(addonDeps) do if (dependancy:lower() == "stubby") then requiresLoad = true end end end if (requiresLoad) then inspectAddOn(name, title, notes) end end end -- This function runs through the boot scripts we have, and if the -- related addon is not loaded yet, runs the boot script. function runBootCodes() if (not StubbyConfig.boots) then return end for addon, boots in pairs(StubbyConfig.boots) do if (not IsAddOnLoaded(addon) and IsAddOnLoadOnDemand(addon)) then local _, _, _, _, loadable = GetAddOnInfo(addon) if (loadable) then for bootname, boot in pairs(boots) do RunScript(boot) end end end end end function onWorldStart() -- Check for expired or updated addons and remove their boot codes. checkAddOns() -- Run all of our boots to setup the respective addons functions. runBootCodes() -- The search for new life and new civilizations... or just addons maybe. searchForNewAddOns() -- Delete data for removed addons cleanUpAddOnData() end function onLoaded() if (not (type(StubbyConfig) == "table")) then StubbyConfig = {} end if (not StubbyConfig.inspected) then StubbyConfig.inspected = {} end if (not StubbyConfig.addinfo) then StubbyConfig.addinfo = {} end Stubby.RegisterEventHook("PLAYER_LOGIN", "Stubby", onWorldStart) end function events(event, ...) if (not event) then event = "" end local firstArg = ... if (event == "ADDON_LOADED") then if (firstArg and (firstArg:lower() == "stubby")) then onLoaded() end Stubby.LoadWatcher(...) end Stubby.EventWatcher(event, ...) end function chatPrint(...) if ( DEFAULT_CHAT_FRAME ) then local msg = "" for i = 1, select("#", ...) do if (i == 1) then msg = select(i, ...) else msg = msg.." "..select(i, ...) end end DEFAULT_CHAT_FRAME:AddMessage(msg, 1.0, 0.35, 0.15) end end -- This function allows boot code to store a configuration variable -- by default the variable is per character unless isGlobal is set. function setConfig(ownerAddOn, variable, value, isGlobal) local ownerIndex = ownerAddOn:lower() local varIndex = variable:lower() if (not isGlobal) then varIndex = UnitName("player"):lower() .. ":" .. varIndex end if (not StubbyConfig.configs) then StubbyConfig.configs = {} end if (not StubbyConfig.configs[ownerIndex]) then StubbyConfig.configs[ownerIndex] = {} end StubbyConfig.configs[ownerIndex][varIndex] = value end -- This function gets a config variable stored by the above function -- it will prefer a player specific variable over a global with the -- same name function getConfig(ownerAddOn, variable) local ownerIndex = ownerAddOn:lower() local globalIndex = variable:lower() local playerIndex = UnitName("player"):lower() .. ":" .. globalIndex if (not StubbyConfig.configs) then return end if (not StubbyConfig.configs[ownerIndex]) then return end local curValue = StubbyConfig.configs[ownerIndex][playerIndex] if (curValue == nil) then curValue = StubbyConfig.configs[ownerIndex][globalIndex] end return curValue end -- This function clears the config variable specified (both the -- global and player specific) or all config variables for the -- ownerAddOn if no variable is specified function clearConfig(ownerAddOn, variable) local ownerIndex = ownerAddOn:lower() if (not StubbyConfig.configs) then return end if (not StubbyConfig.configs[ownerIndex]) then return end if (variable) then local globalIndex = variable:lower() local playerIndex = UnitName("player"):lower() .. ":" .. globalIndex StubbyConfig.configs[ownerIndex][globalIndex] = nil StubbyConfig.configs[ownerIndex][playerIndex] = nil else StubbyConfig.configs[ownerIndex] = nil end end -- Extract the revision number from SVN keyword string function getRevision() return tonumber(("$Revision: 275 $"):match("(%d+)")) end ------------------------------------------------------------------------------- -- This function is a modified and slower version of the table.remove() -- function. It is designed to work with lists even those which contain holes. -- -- The function removes the element at the given position, which can be any key. -- The removed value is returned. In advance if the key is a number greater than -- zero, all elements starting at [pos+1] to table.max(n) will be shifted by -- one index. -- All elements with either negative, or hashed keys (i.e. keys which use -- strings, functions, or anything like this) as well as t[0] are not shifted -- or changed, except if the element is the one to be removed, in which case the -- element is being erased from the table. -- -- If t[pos] does not exist, no elements will be removed, but all elements with -- indexes above pos will be shifted by one. -- -- called by: -- hookCall() - after the original function has been called -- -- parameters: -- t - (list) table to be adjusted -- pos - (number) the position of the element which is to be removed -- the specified number must be greater than 0 -- (anything else) the element at this position will be removed but -- no shifting is being performed -- nil, to remove the element at position: table.maxn(t) -- -- returns: -- The value of the removed element. -- -- remarks: -- The behaviour of table.remove() when working with tables which contain -- holes is undefined. For example {nil, true, nil} will correctly return -- nil when calling table.remove(), but the index of true, is not changed -- and will still be [2] after the function call. -- If this behaviour is not wanted, use this modified version of the -- original function. -- Also note that the runtime of this function is O(n), so use it with -- precaution. ------------------------------------------------------------------------------- function tableRemoveNilSafe(t, pos) pos = pos or table.maxn(t) if pos == nil then pos = table.maxn(t) if pos == 0 then -- the table does not contain any numeric indexes greater than 0, so there -- is nothing todo for us return end end -- clearing the temporary table for key, data in pairs(tempTable) do tempTable[key] = nil end -- retrieve the key from the table and remove it local ret = t[pos] t[pos] = nil -- construct the new table and clear the current one for key, data in pairs(t) do if (type(key) == 'number') and (key > pos) then tempTable[key-1] = data else tempTable[key] = data end t[key] = nil end -- copy the temporary table to the current one for key, data in pairs(tempTable) do t[key] = data end return ret end -- Setup our Stubby global object. All interaction is done -- via the methods exposed here. Stubby = { VERSION = getRevision(), Print = chatPrint, Events = events, HookCall = hookCall, SetConfig = setConfig, GetConfig = getConfig, ClearConfig = clearConfig, GetOrigFunc = getOrigFunc, LoadWatcher = loadWatcher, ErrorHandler = errorHandler, EventWatcher = eventWatcher, RegisterBootCode = registerBootCode, RegisterEventHook = registerEventHook, RegisterAddOnHook = registerAddOnHook, RegisterFunctionHook = registerFunctionHook, UnregisterBootCode = unregisterBootCode, UnregisterEventHook = unregisterEventHook, UnregisterAddOnHook = unregisterAddOnHook, UnregisterFunctionHook = unregisterFunctionHook, CreateAddOnLoadBootCode = createAddOnLoadBootCode, CreateEventLoadBootCode = createEventLoadBootCode, CreateFunctionLoadBootCode = createFunctionLoadBootCode, GetName = function() return "Stubby" end } StubbyHook = { GetName = function() return "Hooked Function" end, }