AuctioneerSuite/Stubby/Stubby.lua
2026-04-13 17:48:13 -04:00

1139 lines
40 KiB
Lua

--[[
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,
}