--[[ Name: AceHook-2.0 Revision: $Rev: 11577 $ Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) Inspired By: Ace 1.x by Turan (turan@gryphon.com) Website: http://www.wowace.com/ Documentation: http://www.wowace.com/index.php/AceHook-2.0 SVN: http://svn.wowace.com/root/trunk/Ace2/AceHook-2.0 Description: Mixin to allow for safe hooking of functions, methods, and scripts. Dependencies: AceLibrary, AceOO-2.0 ]] local MAJOR_VERSION = "AceHook-2.0" local MINOR_VERSION = "$Revision: 11577 $" -- This ensures the code is only executed if the libary doesn't already exist, or is a newer version if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end --[[--------------------------------------------------------------------------------- Create the library object ----------------------------------------------------------------------------------]] local AceOO = AceLibrary:GetInstance("AceOO-2.0") local AceHook = AceOO.Mixin { "Hook", "Unhook", "UnhookAll", "HookReport", "IsHooked", "HookScript", } local table_setn do local version = GetBuildInfo() if string.find(version, "^2%.") then -- 2.0.0 table_setn = function() end else table_setn = table.setn end end --[[--------------------------------------------------------------------------------- Library Definitions ----------------------------------------------------------------------------------]] local protFuncs = { CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true, TurnOrActionStart = true, TurnOrActionStop = true, PitchUpStart = true, PitchUpStop = true, PitchDownStart = true, PitchDownStop = true, MoveBackwardStart = true, MoveBackwardStop = true, MoveForwardStart = true, MoveForwardStop = true, Jump = true, StrafeLeftStart = true, StrafeLeftStop = true, StrafeRightStart = true, StrafeRightStop = true, ToggleMouseMove = true, ToggleRun = true, TurnLeftStart = true, TurnLeftStop = true, TurnRightStart = true, TurnRightStop = true, } local _G = getfenv(0) local handlers, funcs, scripts, actives --[[--------------------------------------------------------------------------------- Private definitions (Not exposed) ----------------------------------------------------------------------------------]] --[[---------------------------------------------------------------------- _debug - Internal Method -------------------------------------------------------------------------]] local function print(text) DEFAULT_CHAT_FRAME:AddMessage(text) end local function _debug(self, msg) local name = self.hooks.name if name then print(string.format("[%s]: %s", name, msg)) else print(msg) end end local new, del do local list = setmetatable({}, {__mode = "k"}) function new() local t = next(list) if not t then return {} end list[t] = nil return t end function del(t) setmetatable(t, nil) table_setn(t, 0) for k in pairs(t) do t[k] = nil end list[t] = true end end local origMetatable = { __call = function(self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) return self.orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) end } --[[---------------------------------------------------------------------- AceHook:_getFunctionHook- internal method -------------------------------------------------------------------------]] local function _getFunctionHook(self, func, handler, orig) if type(handler) == "string" then -- The handler is a method, need to self it return function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) if actives[orig] then return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) else return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) end end else -- The handler is a function, just call it return function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) if actives[orig] then return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) else return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) end end end end --[[---------------------------------------------------------------------- AceHook:_getMethodHook - Internal Method -------------------------------------------------------------------------]] local function _getMethodHook(self, object, method, handler, orig, script) if type(handler) == "string" then -- The handler is a method, need to self it if script then return function() if actives[orig] then return self[handler](self, object) else return orig() end end else return function(obj,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) if actives[orig] then return self[handler](self, obj, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) else return orig(obj, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) end end end else -- The handler is a function, just call it if script then return function() if actives[orig] then return handler(object) else return orig() end end else return function(obj,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) if actives[orig] then return handler(obj,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) else return orig(obj, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) end end end end end --[[---------------------------------------------------------------------- AceHook:HookFunc - internal method. o You can only hook each function once from each source. o If there is an inactive hook for this func/handler pair, we reactivate it o If there is an inactive hook for another handler, we error out. o Looks for handler as a method of the calling class, error if not available o If handler is a function, it just uses it directly through the wrapper -------------------------------------------------------------------------]] local function _hookFunc(self, func, handler) local orig = _G[func] if not orig or type(orig) ~= "function" then _debug(self, string.format("Attempt to hook a non-existant function %q", func),3) return end if not handler then handler = func end if self.hooks[func] then local orig = self.hooks[func].orig -- We have an active hook from this source. Don't multi-hook if actives[orig] then _debug(self, string.format("%q already has an active hook from this source.", func)) return end -- The hook is inactive, so reactivate it if handlers[orig] == handler then actives[orig] = true return else AceHook:error("There is a stale hook for %q can't hook or reactivate.", func) end end if type(handler) == "string" then if type(self[handler]) ~= "function" then AceHook:error("Could not find the the handler %q when hooking function %q", handler, func) end elseif type(handler) ~= "function" then AceHook:error("Could not find the handler you supplied when hooking %q", func) end local t = setmetatable(new(), origMetatable) self.hooks[func] = t t.orig = orig actives[orig] = true handlers[orig] = handler local newFunc = _getFunctionHook(self, func, handler, orig) funcs[orig] = newFunc _G[func] = newFunc end --[[---------------------------------------------------------------------- AceHook:UnhookFunc - internal method o If you attempt to unhook a function that has never been hooked, or to unhook in a system that has never had a hook before, the system will error with a stack trace o If we own the global function, then put the original back in its place and remove all references to the Hooks[func] structure. o If we don't own the global function (we've been hooked) we deactivate the hook, forcing the handler to passthrough. -------------------------------------------------------------------------]] local function _unhookFunc(self, func) if not self.hooks[func] or not funcs[self.hooks[func].orig] then _debug(self, string.format("Tried to unhook %q which is not currently hooked.", func)) return end local orig = self.hooks[func].orig if actives[orig] then -- See if we own the global function if _G[func] == funcs[orig] then _G[func] = orig self.hooks[func] = del(self.hooks[func]) handlers[orig] = nil funcs[orig] = nil scripts[orig] = nil actives[orig] = nil -- Magically all-done else actives[orig] = nil end end end --[[---------------------------------------------------------------------- AceHook:HookMeth - Takes an optional fourth argument o script - Signifies whether this is a script hook or not -------------------------------------------------------------------------]] local function _hookMeth(self, obj, method, handler, script) if not handler then handler = method end if (not obj or type(obj) ~= "table") then AceHook:error("The object you supplied could not be found, or isn't a table.") end if self.hooks[obj] and self.hooks[obj][method] then local orig = self.hooks[obj][method].orig -- We have an active hook from this source. Don't multi-hook if actives[orig] then _debug(self, string.format("%q already has an active hook from this source.", method)) return end -- The hook is inactive, so reactivate it. if handlers[orig] == handler then actives[orig] = true return else AceHook:error("There is a stale hook for %q can't hook or reactivate.", method) end end -- We're clear to try the hook, let's make some checks first if type(handler) == "string" then if type(self[handler]) ~= "function" then AceHook:error("Could not find the handler %q you supplied when hooking method %q", handler, method) end elseif type(handler) ~= "function" then AceHook:error("Could not find the handler you supplied when hooking method %q", method) end -- Handler has been found, so now try to find the method we're trying to hook local orig -- Script if script then if not obj.GetScript then AceHook:error("The object you supplied does not have a GetScript method.") end if not obj:HasScript(method) then AceHook:error("The object you supplied doesn't allow the %q method.", method) end -- Sometimes there is not a original function for a script. orig = obj:GetScript(method) if not orig then orig = function() end end -- Method else orig = obj[method] end if not orig then AceHook:error("Could not find the method or script %q you are trying to hook.", method) end if not self.hooks[obj] then self.hooks[obj] = new() end local t = setmetatable(new(), origMetatable) self.hooks[obj][method] = t t.orig = orig actives[orig] = true handlers[orig] = handler scripts[orig] = script and true or nil local newFunc = _getMethodHook(self, obj, method, handler, orig, script) funcs[orig] = newFunc if script then obj:SetScript(method, newFunc) else obj[method] = newFunc end end --[[---------------------------------------------------------------------- AceHook:UnhookMeth - Internal method o If you attempt to unhook a method that has never been hooked, or to unhook in a system that has never had a hook before, the system will error with a stack trace o If we own the global method, then put the original back in its place and remove all references to the Hooks[obj][method] structure. o If we don't own the global method (we've been hooked) we deactivate the hook, forcing the handler to passthrough. -------------------------------------------------------------------------]] local function _unhookMeth(self, obj, method) if not self.hooks[obj] or not self.hooks[obj][method] or not funcs[self.hooks[obj][method].orig] then _debug(self, string.format("Attempt to unhook a method %q that is not currently hooked.", method)) return end local orig = self.hooks[obj][method].orig if actives[orig] then -- If this is a script if scripts[orig] then if obj:GetScript(method) == funcs[orig] then -- We own the script. Kill it. obj:SetScript(method, orig) self.hooks[obj][method] = del(self.hooks[obj][method]) handlers[orig] = nil funcs[orig] = nil scripts[orig] = nil actives[orig] = nil else actives[orig] = nil end else if obj[method] == funcs[orig] then -- We own the method. Kill it. obj[method] = orig self.hooks[obj][method] = del(self.hooks[obj][method]) handlers[orig] = nil funcs[orig] = nil scripts[orig] = nil actives[orig] = nil else actives[orig] = nil end end end if not next(self.hooks[obj]) then -- Spank the table self.hooks[obj] = del(self.hooks[obj]) end end function AceHook:OnInstanceInit(object) if not object.hooks then object.hooks = new() end local name if type(rawget(object, 'GetLibraryVersion')) == "function" then name = object:GetLibraryVersion() end if not name and type(object.GetName) == "function" then name = object:GetName() end if not name and type(object.name) == "string" then name = object.name end if not name then for k,v in pairs(_G) do if v == object then name = tostring(k) break end end end object.hooks.name = name end AceHook.OnManualEmbed = AceHook.OnInstanceInit --[[---------------------------------------------------------------------- AceHook:Hook self:Hook("functionName", ["handlerName" | handler]) self:Hook(ObjectName, "Method", ["Handler" | handler]) -------------------------------------------------------------------------]] function AceHook:Hook(arg1, arg2, arg3) if type(arg1)== "string" then if protFuncs[arg1] then if self.hooks.name then AceHook:error("%s tried to hook %q, which is a Blizzard protected function.", self.hooks.name, arg1) else _debug(self, string.format("An Addon tried to hook %q, which is a Blizzard protected function.", arg1)) end else _hookFunc(self, arg1, arg2) end else _hookMeth(self, arg1, arg2, arg3) end end function AceHook:HookScript(arg1, arg2, arg3) _hookMeth(self, arg1, arg2, arg3, true) end --[[---------------------------------------------------------------------- AceHook:IsHooked() self:Hook("functionName") self:Hook(ObjectName, "Method") Returns whether or not the given function is hooked in the current namespace. A hooked, but inactive function is considered NOT hooked in this context. -------------------------------------------------------------------------]] function AceHook:IsHooked(obj, method) if method and obj then if self.hooks and self.hooks[obj] and self.hooks[obj][method] and actives[self.hooks[obj][method].orig] then return true, handlers[self.hooks[obj][method].orig] end else if self.hooks and self.hooks[obj] and actives[self.hooks[obj].orig] then return true, handlers[self.hooks[obj].orig] end end return false, nil end --[[---------------------------------------------------------------------- AceHook:Unhook self:Unhook("functionName") self:Unhook(ObjectName, "Method") -------------------------------------------------------------------------]] function AceHook:Unhook(arg1, arg2) if type(arg1) == "string" then _unhookFunc(self, arg1) else _unhookMeth(self, arg1, arg2) end end --[[---------------------------------------------------------------------- AceHook:UnhookAll - Unhooks all active hooks from the calling source -------------------------------------------------------------------------]] function AceHook:UnhookAll() for key, value in pairs(self.hooks) do if type(key) == "table" then for method in pairs(value) do self:Unhook(key, method) end else self:Unhook(key) end end end function AceHook:OnEmbedDisable(target) self.UnhookAll(target) end --[[---------------------------------------------------------------------- AceHook:HookReport - Lists registered hooks from this source -------------------------------------------------------------------------]] function AceHook:HookReport() _debug(self, "This is a list of all active hooks for this object:") if not self.hooks then _debug(self, "No registered hooks.") return end for key, value in pairs(self.hooks) do if type(value) == "table" then for method in pairs(value) do _debug(self, string.format("key: %s method: %q |cff%s|r", tostring(key), method, self.hooks[key][method].active and "00ff00Active" or "ffff00Inactive")) end else _debug(self, string.format("key: %s value: %q |cff%s|r", tostring(key), tostring(value), self.hooks[key].active and "00ff00Active" or "ffff00Inactive")) end end end --[[--------------------------------------------------------------------------------- Stub and Library registration ----------------------------------------------------------------------------------]] local function activate(self, oldLib, oldDeactivate) AceHook = self AceHook.super.activate(self, oldLib, oldDeactivate) if oldLib then self.handlers = oldLib.handlers self.funcs = oldLib.funcs self.scripts = oldLib.scripts self.actives = oldLib.actives end if not self.handlers then self.handlers = {} end if not self.funcs then self.funcs = {} end if not self.scripts then self.scripts = {} end if not self.actives then self.actives = {} end handlers = self.handlers funcs = self.funcs scripts = self.scripts actives = self.actives if oldDeactivate then oldDeactivate(oldLib) end end AceLibrary:Register(AceHook, MAJOR_VERSION, MINOR_VERSION, activate)