Thread Tools Display Modes
11-14-05, 12:06 PM   #1
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Can API fcns be hooked?

I was trying to write a little experimental mod that involves hooking CastSpell and CastSpellByName. I have the replacement procedures add a message to the default chat window, bump a counter, and then call the original procedure with all the original parms.

When I examine the procedure values after loading, they are as I would expect them to be, in the sense that CastSpell and CastSpellByName are equal to my versions of the same, and the original values that I stored away are different (and happen to have lower values).

BUT, when I take my character into combat and use my special rogue moves (Gouge, Backstab, etc) no extra message are displayed to the chat window, and the counter is not being bumped.

Can anyone please help me understand what's going on? Is it not possible to truly hook procedures that are part of the core API? Here's a code snippet below.

--Qzot


Code:
function POSC_CastSpell(...)
	POSC:log('CastSpell called.');
	POSC.used = POSC.used+1;
	POSC.originalCastSpell(unpack(arg));
end

function POSC_CastSpellByName(...)
	POSC:log('CastSpellByName called.');
	POSC.used = POSC.used+1;
	POSC.originalCastSpellByName(unpack(arg));
end

function POSC:OnLoad()
	self.originalCastSpell = CastSpell;
	CastSpell = POSC_CastSpell;
	self.originalCastSpellByName = CastSpellByName;
	CastSpellByName = POSC_CastSpellByName;
	self:log('Loaded.');
end

function POSC:logCurrentProcedureValues()
	self:log('CastSpell=', CastSpell);
	self:log('POSC_CastSpell=', POSC_CastSpell);
	self:log('POSC.originalCastSpell=', POSC.originalCastSpell);

	self:log('CastSpellByName=', CastSpellByName);
	self:log('POSC_CastSpellByName=', POSC_CastSpellByName);
	self:log('POSC.originalCastSpellByName=', POSC.originalCastSpellByName);
end

POSC:OnLoad();
  Reply With Quote
11-14-05, 01:27 PM   #2
Esamynn
Featured Artist
Premium Member
Featured
Join Date: Jan 2005
Posts: 395
I would suspect that functions like CastSpell that are defined in the complied code instead on in the UI LUA code are not changeable on purpose for integrity reasons.
  Reply With Quote
11-14-05, 03:36 PM   #3
Beladona
A Molten Giant
 
Beladona's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2005
Posts: 539
There is nothing stopping you from hooking those functions. The problem you are having are as follows:

:log? what the heck is this. I don't see it defined in your code.

You keep using : and . interchangeable. This works to some degree the way you used it, but there is absolutely no reason for you to use it. For example:

table:item(...)
is the same as
table.item(self, ...)

The only time you used it was when you wanted to pass POSC as self. You could just as easily replace self with POSC and make it easier for anyone else looking at your code to understand. It also makes bugs easier to track in the long run. I stay away from passing variables for things that never change anyway.

Which brings me to the other issue. POSC should be getting defined as a table initially, and then add stuff to it. POSC might be the name of your frame, in which case it is already a table. I have no way of knowing that, but if that is the case, I wouldn't pack too much into it anyway. Create POSC as a lua table, and then make your frame POSC_Frame or something to that effect.

Here is an example:

Code:
POSC = {}; -- your new table

POSC.OnLoad = function()
	POSC.oldCastSpell = CastSpell; -- backup original function
	CastSpell = POSC.CastSpell; -- replate original with custom
	POSC.oldCastSpellByName = CastSpellByName; -- backup original function
	CastSpellByName = POSC.CastSpellByName; -- replace original with custom
	POSC.Log("Loaded..."); -- output to chat log?
	POSC.Used = 0; -- define this once, so that it can start counting up from 0
end;

POSC.CastSpell = function(...)
	POSC.Log("CastSpell Called.");
	POSC.Used = Posc.Used + 1; -- add to the counter
	POSC.oldCastSpell(unpack(arg));
end;

POSC.CastSpellByName = function(...)
	POSC.Log("CastSpellByName Called.");
	POSC.Used = POSC.Used + 1; -- add to the counter
	POSC.oldCastSpellByName(unpack(arg));
end;

POSC.Log = function(text)
	-- I assume this is a function to output the text argument to the chat window?
	-- Replace this with the actual function...
end;

POSC.OnLoad;
What you see above may or may not work out of the box. I make no guarantees. I didn't test it, and as a result leave it up to you to experiement with. However what you see above is MUCH closer to being what you need than before...

Last edited by Beladona : 11-14-05 at 04:19 PM.
  Reply With Quote
11-14-05, 04:18 PM   #4
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Beladona,

I do define POSC before this code, and I have a log function defined. I only showed a 'snippet', as stated, and assumed people would fill in the blanks for what came before in the file.

POSC is not the name of my frame. In fact, my .toc does not reference a frame, but only a lua file, which is why my code contains a call to POSC:OnLoad() rather than having the xml generate the onload call.

As to strings, the Lua reference manual (http://www.lua.org/manual/5.0/manual.html#2) says that literal strings may be delimited by single or double quotes, and makes no distinction between the two. I'm not quite sure what you mean about "correct Lua".

I prefer to use : syntax when possible. For example, my log function uses field of POSC which store the package name and version for the log message.

For complete reference, here is the portion of the code which was not included in the previous snippet. While the stylistic preferences may be different that what someone else might choose, I still don't quite understand why spell casts are not being caught by the hooked procedures.

Code:
--[[

	Copyright (c) Palo Alto Research Center, Inc. ("PARC"), 2005. 
	All rights reserved.
	
	This program is free software; you can redistribute it and/or modify it
	under the terms of version 2 of the GNU General Public License as published by the
	Free Software Foundation.
	
	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; if not, write to the Free Software Foundation, Inc.,
	51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
	
	This file contributed by the Play-On Project at PARC <[email protected]>
]]--

local PGM_NAME, PGM_TITLE, PGM_NOTES, PGM_ENABLED, PGM_LOADABLE, PGM_REASON, PGM_SECURITY
	= GetAddOnInfo("PO_SpellCorrector");
local _,_,PROGRAM_NAME = string.find(PGM_NAME, "PO_([^_]+)");
local _,_,PROGRAM_VERSION = string.find(PGM_TITLE, "<([%w\\.]+)>");
	if (PROGRAM_VERSION == nil) then PROGRAM_VERSION = "<unknown>"; end;
local PROGRAM_NAME_AND_VERSION = PROGRAM_NAME .. PROGRAM_VERSION;
local realmName = GetCVar("realmName");

POSC = {used=0};

function POSC:full_fmt(...)
	local result = "";
	for i,v in ipairs(arg) do
		result = result .. tostring(v);
	end
	return result;
end

function POSC:msg(...)
	if (ChatFrame1) then
		ChatFrame1:AddMessage(self:full_fmt(unpack(arg)), 1, 0.8, 0.4);
	end
end

function POSC:log(...)
	local prefix = 
		date("%H:%M:%S") 
--		.. " " .. realmName 
		.. " " .. PROGRAM_NAME_AND_VERSION .. ": ";
	local line = self:full_fmt(prefix, unpack(arg));
	self:msg(line);
	if (OBSERVATIONS and OBSERVATIONS.LOGS) then
		if (nil == OBSERVATIONS.LOGS[filename]) then
			OBSERVATIONS.LOGS[filename] = {};
			local header = self:full_fmt(date("%H:%M:%S "), " ", " Logging started.",
				date(), realmName, UnitName('player'), GetRealZoneText());
			table.insert(OBSERVATIONS.LOGS[filename], string.rep("-", string.len(header)));
			table.insert(OBSERVATIONS.LOGS[filename], header);
		end
		table.insert(OBSERVATIONS.LOGS[filename], line);
	end
end

function POSC:Select(condition, iftrue, iffalse)
	if (condition) then
		return iftrue;
	else
		return iffalse;
	end
end

function POSC:IfNil(value, default)
	return self:Select(value == nil, default, value);
end

function POSC_CastSpell(...)
	POSC:log('CastSpell called.');
	POSC.used = POSC.used+1;
	POSC.originalCastSpell(unpack(arg));
end

function POSC_CastSpellByName(...)
	POSC:log('CastSpellByName called.');
	POSC.used = POSC.used+1;
	POSC.originalCastSpellByName(unpack(arg));
end

function POSC:OnLoad()
	self.originalCastSpell = CastSpell;
	CastSpell = POSC_CastSpell;
	self.originalCastSpellByName = CastSpellByName;
	CastSpellByName = POSC_CastSpellByName;
	self:log('Loaded.');
end

function POSC:logCurrentProcedureValues()
	self:log('CastSpell=', CastSpell);
	self:log('POSC_CastSpell=', POSC_CastSpell);
	self:log('POSC.originalCastSpell=', POSC.originalCastSpell);

	self:log('CastSpellByName=', CastSpellByName);
	self:log('POSC_CastSpellByName=', POSC_CastSpellByName);
	self:log('POSC.originalCastSpellByName=', POSC.originalCastSpellByName);
end

POSC:OnLoad();
  Reply With Quote
11-14-05, 04:29 PM   #5
Beladona
A Molten Giant
 
Beladona's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2005
Posts: 539
whatever works for you. I just see no reason to use : if you don't need to. all it does is pass self as the first argument, which in your case I guess you use it, although not in all instances.

Adding the rest of your code helps a lot to debug the problem, so I will look into it some more and play with it.
  Reply With Quote
11-14-05, 04:33 PM   #6
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Beladona,

Belatedly, I noticed a comment in the code in your reply. (I think you must have been editing your reply while I was replying to your reply -- for example, your comment that double quotes is more correct than single quotes is now gone.)

Anyway, you questioned why I chose to use (...) and then (unpack(arg)) syntax, and even tho it's really OT from my original question, I thought I'd give some sort of an explanation.

Since I'm hooking Blizzard's function, and then calling it (I think Erich Gamma, et al, in Design Patterns call this a "Chain of Responsibility" pattern), my function becomes responsible for passing all the parameters it receives to the original function. While the syntax I chose is messier, it does exactly that: it guarantees to send to the original function exactly what it received. I admit this would only have limited utility ... for example, if Blizzard started optionally passing an extra parameter in some future release, this would still work correctly.

I got into the habit of coding hooked procedures this way after seeing parameters missing or marked as having unknown meaning on WoWWiki for API functions that I know are there.
  Reply With Quote
11-14-05, 04:34 PM   #7
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Lol.

Looks like we're still passing each other in the ether.

Thanks for taking a look.
  Reply With Quote
11-14-05, 04:37 PM   #8
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
And in the interests of completeness, here's the TOC:

Code:
## Title: PlayOn's SpellCorrector <1800beta01>

## Copyright (c) Palo Alto Research Center, Inc. ("PARC"), 2005. 
## All rights reserved.

## This program is free software; you can redistribute it and/or modify it
## under the terms of version 2 of the GNU General Public License as published by the
## Free Software Foundation.

## 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; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

## This file contributed by the Play-On Project at PARC <[email protected]>

## Interface: 1800 
PO_SpellCorrector.lua
  Reply With Quote
11-14-05, 06:26 PM   #9
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
ARGHH! Here's some more info from experiments...

I've tried out my rogue with 3 different versions of Sinister Strike. When I copy Sinister Strike from the spellbook to the action bar and use it, I don't get any of the reporting via the addon, as listed above.

But, when I create the following macro and use it in the place of Sinister Strike
Code:
/script CastSpell(32, "spell")
/s CallCastSpell
(Here, 32 obviously was empirically determined and only works for my current set of spells. Doing it this way makes sure I'm accessing CastSpell, rather than CastSpellByName.)

I get output something like
Code:
16.08.30 SpellCorrector1800beta01: CastSpell called.
[Foobar] CallCastSpell
for each time I press the macro on the action bar. (Remember that my replacement function tries to add a message to the default chat log.)

And then I have a third form, where the macro looks like the more obvious
Code:
/cast Sinister Strike
in which case I get the output in the chat of
Code:
16.08.12 SpellCorrector1800beta01: CastSpellByName called.
This would seem to indicate that I have successfully hooked CastSpell and CastSpellByName, but that someone is calling the original procedure directly in a way that prevents those calls from being hooked. One way to explain this would be if a piece of the UI did something like
Code:
local myCastSpell = CastSpell;
or the moral equivalent. Assuming it got called before my addon loaded, I would have no way to access the old function value now stored in myCastSpell. Bleah.

Any other thoughts on the problem?
  Reply With Quote
11-15-05, 08:12 AM   #10
Beladona
A Molten Giant
 
Beladona's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2005
Posts: 539
very strange. I may have to load the code myself in-game and see what it does for my warrior. You are right that it looks like using the function innately within the actionbar doesn't seem to call your hooked function...
  Reply With Quote
11-15-05, 08:23 AM   #11
Beladona
A Molten Giant
 
Beladona's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2005
Posts: 539
your problem seems to originate from ActionBarFrame.xml

In this file, the OnClick script is set to the following:

Code:
				if ( IsShiftKeyDown() ) then
					PickupAction(ActionButton_GetPagedID(this));
				else
					if ( MacroFrame_SaveMacro ) then
						MacroFrame_SaveMacro();
					end
					UseAction(ActionButton_GetPagedID(this), 1);
				end
				ActionButton_UpdateState();
Of specific note is the line:
UseAction(ActionButton_GetPagedID(this), 1);

This is the piece of code that actually calls whatever is in the button you are pressing. Unfortuantely it is a global api function. They can be used in lua scripts, but they are not exposed within any lua, meaning they cannot be hooked. Obviously due to it not being exposed we can't see how it is using the CastSpell function, although my guess is that it does not. It probably does everything the original unhooked CastSpell does, and more, but since you can't hook it you can't add any verbose log output...

This falls back to the old "you can't use a skill without hardware input" rule. They didn't expose the function to hooking because doing so would allow automatic macro script to take over...

Last edited by Beladona : 11-15-05 at 08:25 AM.
  Reply With Quote
11-15-05, 11:29 AM   #12
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Great detective work!

Beladona,

Great detective work! I'd been coming pretty much to the same conclusion by reading through the API on WoWWiki, but you've gone the extra mile and isolated it in the xml.

I'll also hook UseAction to make sure that I can do so and see the message, but from a cursory perusal of the API, it's not clear that I can determine which spell is hooked up to which action button. That warrants further explanation.

(Details: It appears possible to know what spell is on which action button if the addon is running at the time the action is loaded onto the action button. But once loaded, there does not appear to be an easy way to distinguish between a macro action and a spell action. Textures could help. Tooltips could help. It all looks ugly.)

Unfortuantely it is a global api function. They can be used in lua scripts, but they are not exposed within any lua, meaning they cannot be hooked.
I'm not sure what you're referring to here, so I don't get what you're saying. Are you saying that the snippet is a global API? That UseAction is? Or UseAction's use of a virtual CastSpell? Please clarify.

This falls back to the old "you can't use a skill without hardware input" rule. They didn't expose the function to hooking because doing so would allow automatic macro script to take over...
I don't see how this follows. How would this open any more holes than allowing us to hook CastSpell or CastSpellByName? They can still do the check at the back end to make sure that no more than one spell cast is loaded onto any hardware event.
  Reply With Quote
11-16-05, 08:22 AM   #13
Beladona
A Molten Giant
 
Beladona's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2005
Posts: 539
I am just saying that some global api functions are locked and cannot be hooked. UseAction might be hookable, but I am not sure. I couldn't find it in any lua scripts at all, which means it is not exposed. Try hooking it to see what happens, as it never hurts to experiement...

Maybe a combination of hooking UseAction, CastSpell, and CastSpellByName will get you what you need...

I would try hooking it myself, but I am on my laptop, in the middle of reinstall, and still need to patch up to 1.8

Last edited by Beladona : 11-16-05 at 09:42 AM.
  Reply With Quote
11-16-05, 08:26 AM   #14
Cladhaire
Salad!
 
Cladhaire's Avatar
Premium Member
AddOn Author - Click to view addons
Join Date: Jul 2005
Posts: 1,935
As posted in the WoW forums in your topic:

Code:
 What you're specifically looking for is the following:

MyMod_oldUseAction = UseAction
function MyMod_newUseAction(a1, a2, a3)
   -- Call the original function
   MyMod_oldUseAction(a1, a2, a3)
   -- Test to see if this is a macro.
   -- If this is a macro, GetActiontext(a1) returns the label
   if GetActiontext(a1) then return end
   MyMod_Tooltip:SetAction(a1)
   local spellName = MyMod_TooltipTextLeft1:GetText()
   DEFAULT_CHAT_FRAME("Just caught a " .. (spellName or "nil"))   
end
UseAction = MyMod_newUseAction
  Reply With Quote
11-16-05, 01:54 PM   #15
Silversage
A Flamescale Wyrmkin
AddOn Author - Click to view addons
Join Date: Nov 2005
Posts: 132
Cladhaire,

Yes, since seeing that post in the WoW forums, I've tried hooking UseAction. This basically works, but involves using a tooltip to determine whether the action stored in a button is a macro or a spellcast, and to find the spell of the name. I'm still experimenting to determine whether I can get the spell level. (Hopefully, since this appears in the tooltip for a spell that's been copied to an action bar, it will be available somewhere.)

Interestingly, if you cast a spell directly from the spellbook, it *does* call CastSpell. Weird.

--Q
  Reply With Quote
11-16-05, 04:42 PM   #16
Grumpey
A Murloc Raider
AddOn Author - Click to view addons
Join Date: Oct 2005
Posts: 5
I Think Right1 has the Rank information, or if it is a racial ability it contains Racial.

Righttext=MyMod_TooltipTextRight1:GetText()

Then you could use string.find to return the rank..

Rank=string.find(Righttext, "Rank %d+")

... I'm just learning LUA and WoW stuff so slap me around if I'm completely off base.

Edit something like this

Code:
	
MyMod_oldUseAction = UseAction
function MyMod_newUseAction(a1, a2, a3)
   -- Call the original function
   MyMod_oldUseAction(a1, a2, a3)
   -- Test to see if this is a macro.
   -- If this is a macro, GetActiontext(a1) returns the label
   if GetActionText(a1) then return end
   MyMod_Tooltip:SetAction(a1)
   local spellName = MyMod_TooltipTextLeft1:GetText()
   local spellRankCheck = MyMod_TooltipTextRight1:GetText()
   DEFAULT_CHAT_FRAME:AddMessage("Just caught a " .. (spellName or "nil"))   
   DEFAULT_CHAT_FRAME:AddMessage("Just caught a " .. (spellRankCheck or "nil"))
   if  not spellRankCheck then return end
   if ((string.find(spellRankCheck, "Rank")==1)) then 
	   DEFAULT_CHAT_FRAME:AddMessage("SpellRank: " .. (spellRankCheck))
   else
	   DEFAULT_CHAT_FRAME:AddMessage("Other Info:" .. (spellRankCheck))	   
   end
  MyMod_TooltipTextRight1:SetText(nil)
  MyMod_TooltipTextLeft1:SetText(nil)
end
UseAction = MyMod_newUseAction

Last edited by Grumpey : 11-16-05 at 09:12 PM.
  Reply With Quote
11-30-05, 10:43 PM   #17
Aquendyn
A Deviate Faerie Dragon
Join Date: Sep 2005
Posts: 12
I'm pretty sure all functions are hookable. If not, then very, very few are not.

UseAction is also found in ActionButton.lua, in ActionButtonDown() and Up().

The spellbook knows all the spells' ids and booktype, so it's not surprising to just use CastSpell, which takes the id and booktype. Getting the spell's name and rank would require another step to get the name from the fontstrings. Besides, most things to do with spells are done with id and booktype.
  Reply With Quote
01-04-06, 02:46 AM   #18
Cyrael
A Murloc Raider
Join Date: Jan 2006
Posts: 7
Originally Posted by Aquendyn
I'm pretty sure all functions are hookable.
That's right. This is a language feature of Lua that is a side effect of functions being first class objects. Any function that can be accessed in Lua can be hooked. This goes for everything - functions created in Lua, functions that extend Lua from a compiled C source, everything. I think Beladona may be a little off on this one.
  Reply With Quote

WoWInterface » Developer Discussions » General Authoring Discussion » Can API fcns be hooked?

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off