vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:
------------------------------------------------------
-- HuntersHelper.lua
------------------------------------------------------
FHH_VERSION = "11000.1";
------------------------------------------------------

-- Saved configuration & info
FHH_Config = { };
FHH_Config.Tooltip = true;

--FHH_AbilityInfo = { };
-- Has the following internal structure:
--              REALM_PLAYER = {
--                      SKILL
--              }

-- Runtime state
FHH_State = { };
FHH_State.RealmPlayer = nil;
FHH_State.TamingCritter = nil;
FHH_State.TamingType = nil;

-- Constants
MAX_REPORTED_ZONES = 4;


function FHH_OnLoad()

        this:RegisterEvent("PLAYER_ENTERING_WORLD");
        this:RegisterEvent("UPDATE_MOUSEOVER_UNIT");

        -- Register Slash Commands
        SLASH_FHH1 = "/huntershelper";
        SLASH_FHH2 = "/hh";
        SlashCmdList["FHH"] = function(msg)
                FHH_ChatCommandHandler(msg);
        end
        
        GFWUtils.Print("Fizzwidget Hunter's Helper "..FHH_VERSION.." initialized!");
        
end

function FHH_OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)

        --DevTools_Dump({event=event, arg1=arg1, arg2=arg2, arg3=arg3, arg4=arg4, arg5=arg5, arg6=arg6, arg7=arg7, arg8=arg8, arg9=arg9});

        if ( event == "PLAYER_ENTERING_WORLD" ) then
                
                _, realClass = UnitClass("player");
                if (realClass == "HUNTER") then
                        -- only do stuff related to taming and checking hunter spells if you're a hunter.
                        this:RegisterEvent("UNIT_AURA");
                        this:RegisterEvent("UNIT_NAME_UPDATE");
                        this:RegisterEvent("CRAFT_SHOW");
                        this:RegisterEvent("CHAT_MSG_SYSTEM");

                        if (FHH_State.RealmPlayer == nil) then
                                FHH_State.RealmPlayer = GetCVar("realmName") .. "." .. UnitName("player");
                        end
                        if (FHH_AbilityInfo == nil or FHH_AbilityInfo[FHH_State.RealmPlayer] == nil or GFWTable.Count(FHH_AbilityInfo[FHH_State.RealmPlayer]) == 0) then
                                
                                if (realClass == "HUNTER" and UnitLevel("player") > 9) then
                                        GFWUtils.Print("Hunter's Helper needs to collect info about what pet skills you already know; please open your Beast Training window. (Info on future skills will be collected as they are learned.)");
                                end
                        end
                end
                
        elseif ( event == "UPDATE_MOUSEOVER_UNIT" ) then
        
                if ( UnitExists("mouseover") and not UnitPlayerControlled("mouseover") and FHH_Config.Tooltip ) then

                        local _, myClass = UnitClass("player");
                        if (FHH_Config.Tooltip == "hunter" and myClass ~= "HUNTER") then return; end
                        
                        FHH_ModifyTooltip("mouseover");

                end
                
        elseif ( event == "UNIT_AURA" ) then
        
                if ( arg1 == "player" and FHH_HasTameEffect("player") ) then
                        FHH_State.TamingCritter = UnitName("target");
                        local unlocalizedCreepName = GFWTable.KeyOf(FHH_Localized, FHH_State.TamingCritter);
                        if (unlocalizedCreepName) then
                                FHH_State.TamingCritter = unlocalizedCreepName;
                        end
                        FHH_State.TamingType = UnitClassification("target");
                end
                        
        elseif ( event == "UNIT_NAME_UPDATE" ) then
        
                if ( arg1 == "pet" and FHH_State.TamingCritter ) then
                        local loyaltyDescription = GetPetLoyalty();
                        if (loyaltyDescription) then
                                local _, _, loyaltyLevel = string.find(loyaltyDescription, "(%d+)");
                                if (tonumber(loyaltyLevel) and tonumber(loyaltyLevel) > 1) then
                                        GFWUtils.Print("Got "..event.." but pet's loyalty > 1; ignoring.");
                                        FHH_State.TamingCritter = nil;
                                        FHH_State.TamingType = nil;
                                        return;
                                end             
                        end
                        if (UnitName("pet") ~= UnitCreatureFamily("pet")) then
                                GFWUtils.Print("Got "..event.." but pet's UnitName() ~= UnitCreatureFamily(); ignoring.");
                                FHH_State.TamingCritter = nil;
                                FHH_State.TamingType = nil;
                                return;
                        end
                        --GFWUtils.Print(event..": checking newly tamed pet");
                        FHH_CheckPetSpells();
                        FHH_State.TamingCritter = nil;
                        FHH_State.TamingType = nil;
                end

        elseif ( event == "CRAFT_SHOW" ) then

                -- Beast Training uses the CraftFrame; we can tell it's not really a craft because it doesn't have a skill-level bar.
                local name, rank, maxRank = GetCraftDisplaySkillLine();
                if ( name ) then return; end
                
                FHH_ScanCraftFrame();

        elseif ( event == "CHAT_MSG_SYSTEM" ) then

                local pattern = GFWUtils.FormatToPattern(ERR_LEARN_SPELL_S); -- "You have learned a new spell: %s."
                local _, _, compositeSpellName = string.find(arg1, pattern);
                if (compositeSpellName == nil) then return; end
                
                local _, _, spellName, rankNum = string.find(compositeSpellName, "(.+) %(.+ (%d+)%)");
                if (spellName and rankNum and spellName ~= "" and rankNum ~= "" ) then
                        spellName = string.gsub(spellName, "^%s+", ""); -- strip leading spaces
                        spellName = string.gsub(spellName, "%s+$", ""); -- and trailing spaces
                        local spellID = FHH_SpellIDforName(spellName);
                        if (FHH_NewInfo and FHH_NewInfo.SpellIDAliases and FHH_NewInfo.SpellIDAliases[spellID]) then
                                spellID = FHH_NewInfo.SpellIDAliases[spellID];
                        end
                        if (spellID and (FHH_RequiredLevel[spellID] or (FHH_NewInfo and FHH_NewInfo.RequiredLevel and FHH_NewInfo.RequiredLevel[spellID]))) then
                                -- only track spells we know are hunter pet spells
                                if (FHH_AbilityInfo == nil) then
                                        FHH_AbilityInfo = {};
                                end
                                if (FHH_AbilityInfo[FHH_State.RealmPlayer] == nil or table.getn(FHH_AbilityInfo[FHH_State.RealmPlayer]) == 0) then
                                        FHH_AbilityInfo[FHH_State.RealmPlayer] = { };
                                end
                                table.insert(FHH_AbilityInfo[FHH_State.RealmPlayer], spellName.." "..rankNum);
                                table.sort(FHH_AbilityInfo[FHH_State.RealmPlayer]);
                        end
                end
                
        end

end

function FHH_ChatCommandHandler(msg)

        -- Print Help
        if ( msg == "help" ) or ( msg == "" ) then
                GFWUtils.Print("Fizzwidget Hunter's Helper "..FHH_VERSION..":");
                GFWUtils.Print("/huntershelper /hh <command>");
                GFWUtils.Print("- "..GFWUtils.Hilite("help").." - Print this helplist.");
                GFWUtils.Print("- "..GFWUtils.Hilite("on").." | "..GFWUtils.Hilite("off").." | "..GFWUtils.Hilite("onlyhunter").." - Turn display of beast abilities in tooltips on or off, or make them only appear if you're playing a hunter.");
                GFWUtils.Print("- "..GFWUtils.Hilite("status").." - Check current settings.");
                GFWUtils.Print("- "..GFWUtils.Hilite("find <ability> <rank>").." - List where beasts with a given ability (e.g. Bite 6) can be found.");
                return;
        end

        if (msg == "version") then
                GFWUtils.Print("Fizzwidget Hunter's Helper "..FHH_VERSION);
                return;
        end
                
        if (msg == "onlyhunter") then
                FHH_Config.Tooltip = "hunter";
                GFWUtils.Print("Hunter's Helper is enabled; beast tooltips will display ability info only when playing a Hunter character.");
                return;
        end
        if (msg == "on") then
                FHH_Config.Tooltip = true;
                GFWUtils.Print("Hunter's Helper is enabled; beast tooltips will display ability info.");
                return;
        end
        if (msg == "off") then
                FHH_Config.Tooltip = nil;
                GFWUtils.Print("Hunter's Helper is disabled; no extra info added to tooltips.");
                return;
        end

        -- Check Status
        if ( msg == "status" ) then
                if ( FHH_Config.Tooltip == "hunter" ) then
                        GFWUtils.Print("Hunter's Helper is enabled; beast tooltips will display ability info only when playing as a Hunter.");
                elseif ( FHH_Config.Tooltip ) then
                        GFWUtils.Print("Hunter's Helper is enabled; beast tooltips will display ability info.");
                else
                        GFWUtils.Print("Hunter's Helper is disabled; no extra info added to tooltips.");
                end
                return;
        end
        
        if (msg == "reset") then
                FHH_Config = { };
                FHH_Config.Tooltip = true;
                FHH_AbilityInfo = nil;                          
                FHH_NewInfo = nil;      
                                
                GFWUtils.Print("Hunter's Helper has been reset to default options and all stored data cleared.");
                return;
        end
                
        if (msg == "test") then
                FHH_RunAllTests();
                return;
        end
                
        if (msg == "dynamic") then
                FHH_SpellNamesToIDs = {};
                FHH_SpellIDsToNames = {};
                FHH_LearnableBy = {};
                FHH_RequiredLevel = {};
                FHH_SpellInfo = {};
                FHH_BeastInfo = {};
                FHH_BeastLevels = {};
                GFWUtils.Print("Hunter's Helper: only consulting dynamic tables until next reload.");
                return;
        end
                
        local _, _, cmd, spellName, rankNum = string.find(msg, "(find%w*) ([^%d]+) *(%d*)");
        if (cmd == "find" or cmd == "findall") then
                if (spellName == nil or spellName == "") then
                        GFWUtils.Print("Usage: "..GFWUtils.Hilite("/hh find <ability> <rank>"));
                        return;
                end
                
                spellName = string.gsub(spellName, "^%s+", ""); -- strip leading spaces
                spellName = string.gsub(spellName, "%s+$", ""); -- and trailing spaces
                spellName = string.lower(spellName);
                local spellID;
                
                -- first, look up the input against our spell ID keys
                if (FHH_SpellIDsToNames[spellName]) then
                        spellID = spellName;
                end
                if (spellID == nil and FHH_NewInfo and FHH_NewInfo.SpellIDsToNames and FHH_NewInfo.SpellIDsToNames[spellName]) then
                        spellID = spellName;
                end

                -- failing that, try looking it up as a proper name, case insensitively
                if (spellID == nil) then
                        for properName in FHH_SpellNamesToIDs do
                                if (string.lower(properName) == spellName) then
                                        spellID = FHH_SpellNamesToIDs[properName];
                                end
                        end
                        if (spellID == nil and FHH_NewInfo and FHH_NewInfo.SpellNamesToIDs) then
                                for properName in FHH_NewInfo.SpellNamesToIDs do
                                        if (string.lower(properName) == spellName) then
                                                spellID = FHH_NewInfo.SpellNamesToIDs[properName];
                                        end
                                end
                        end
                end
                
                if (spellID == nil) then
                        GFWUtils.Print(GFWUtils.Hilite(spellName).." is not a known beast ability.");
                        return;
                end
                FHH_Find(spellID, rankNum);
                return;
        end
        
        -- if we got all the way to here, we got invalid input.
        FHH_ChatCommandHandler("help");
        
end

function FHH_ModifyTooltip(unit)
        local creepName = UnitName(unit);
        local creepLevel = UnitLevel(unit);
        local creepFamily = UnitCreatureFamily(unit);
        local creepType = UnitClassification(unit);
        local abilitiesLine;

        local unlocalizedCreepName = GFWTable.KeyOf(FHH_Localized, creepName);
        if (unlocalizedCreepName) then
                creepName = unlocalizedCreepName;
        end
        
        -- if this beast is in our database, make sure we have the right level range & type info
        FHH_CheckBeastLevel(creepName, creepLevel, creepType);

        -- if this is a Beast Lore tooltip, parse out and use its tamed abilities info
        if (FHH_TAMED_ABILS_PATTERN == nil) then
                FHH_TAMED_ABILS_PATTERN = GFWUtils.FormatToPattern(PET_SPELLS_TEMPLATE);
        end
        for lineNum = 1, GameTooltip:NumLines() do
                local lineText = getglobal("GameTooltipTextLeft"..lineNum):GetText();
                if (lineText) then 
                        if (string.find(lineText, LIGHTYELLOW_FONT_COLOR_CODE)) then
                                return; -- if we've already added a line to this tooltip, we should stop.
                        end
                        local _, _, beastLoreInfo = string.find(lineText, FHH_TAMED_ABILS_PATTERN);
                        if (beastLoreInfo) then
                                abilitiesLine = lineNum;
                                local beastLoreList = GFWUtils.Split(beastLoreInfo, ", ");
                                local beastSpellTable = {};
                                for _, niceSpellName in beastLoreList do
                                        local _, _, spellName, rankNum = string.find(niceSpellName, "^(.+) %(.+ (%d+)%)$");
                                        if (spellName == nil or spellName == "" or tonumber(rankNum) == nil) then
                                                GFWUtils.PrintOnce(GFWUtils.Red("Hunter's Helper Error: ").."Can't parse spell "..GFWUtils.Hilite(niceSpellName).." from "..GFWUtils.Hilite(critter)..".");
                                        else
                                                spellName = string.gsub(spellName, "^%s+", ""); -- strip leading spaces
                                                spellName = string.gsub(spellName, "%s+$", ""); -- and trailing spaces
                                                local spellID = FHH_SpellIDforName(spellName);
                                                if (FHH_NewInfo and FHH_NewInfo.SpellIDAliases and FHH_NewInfo.SpellIDAliases[spellID]) then
                                                        spellID = FHH_NewInfo.SpellIDAliases[spellID];
                                                end
                                                if (spellID == nil) then
                                                        spellID = FHH_RecordNewSpellID(spellName, true);
                                                end
                                                beastSpellTable[spellID] = tonumber(rankNum);
                                        end
                                end
                                FHH_CheckSpellTables(creepName, beastSpellTable, creepLevel, creepFamily);
                        end
                end
        end

        -- look up the list of abilities we think this critter has
        local abilitiesList = nil;
        if (FHH_NewInfo and FHH_NewInfo.BeastInfo and FHH_NewInfo.BeastInfo[creepName]) then
                abilitiesList = FHH_NewInfo.BeastInfo[creepName];
        elseif (FHH_BeastInfo[creepName]) then
                abilitiesList = FHH_BeastInfo[creepName];
                if (FHH_NewInfo and FHH_NewInfo.BadBeastInfo and FHH_NewInfo.BadBeastInfo[creepName]) then
                        local newAbilitiesList = {};
                        for spellID, rankNum in abilitiesList do
                                if (FHH_NewInfo.BadBeastInfo[creepName][spellID] ~= rankNum) then
                                        newAbilitiesList[spellID] = rankNum;
                                end
                        end
                        abilitiesList = newAbilitiesList;
                end
        end
                        
        if (abilitiesList and GFWTable.Count(abilitiesList) > 0) then
        
                -- build textual description from that list (with color coding if you're a hunter)
                local coloredList = {};
                local _, myClass = UnitClass("player");
                for spellName, rankNum in abilitiesList do
                        if (myClass == "HUNTER" and FHH_AbilityInfo and FHH_AbilityInfo[FHH_State.RealmPlayer] and GFWTable.Count(FHH_AbilityInfo[FHH_State.RealmPlayer]) > 0) then
                                local playerRanks = FHH_AbilityInfo[FHH_State.RealmPlayer][spellName];
                                if (playerRanks and GFWTable.IndexOf(playerRanks, rankNum) ~= 0) then
                                        table.insert(coloredList, GRAY_FONT_COLOR_CODE..FHH_SpellDescription(spellName, rankNum)..FONT_COLOR_CODE_CLOSE);
                                else
                                        table.insert(coloredList, GREEN_FONT_COLOR_CODE..FHH_SpellDescription(spellName, rankNum)..FONT_COLOR_CODE_CLOSE);
                                end
                        else
                                table.insert(coloredList, FHH_SpellDescription(spellName, rankNum));
                        end
                end
                local abilitiesText = table.concat(coloredList, ", ");
                abilitiesText = string.gsub(abilitiesText, "( %d+)", " ("..RANK.."%1)");
        
                -- add it to the tooltip (or, if Beast Lore, replace its line with our color-coded one)
                if (abilitiesLine) then
                        local lineText = getglobal("GameTooltipTextLeft"..abilitiesLine);
                        lineText:SetText(GFWUtils.LtY(string.format(PET_SPELLS_TEMPLATE, abilitiesText)));
                else
                        GameTooltip:AddLine(GFWUtils.LtY(string.format(PET_SPELLS_TEMPLATE, abilitiesText)), 1.0, 1.0, 1.0);
                        GameTooltip:SetHeight(GameTooltip:GetHeight() + 14);
                        local width = 20 + getglobal(GameTooltip:GetName().."TextLeft"..GameTooltip:NumLines()):GetWidth();
                        if ( GameTooltip:GetWidth() < width ) then
                                GameTooltip:SetWidth(width);
                        end
                end
        end

end

function FHH_ScanCraftFrame()
        local numCrafts = GetNumCrafts();
        if not ( numCrafts > 0 )then return; end

        if (FHH_AbilityInfo == nil) then
                FHH_AbilityInfo = {};
        end
        if (FHH_AbilityInfo[FHH_State.RealmPlayer] == nil or table.getn(FHH_AbilityInfo[FHH_State.RealmPlayer]) == 0) then
                FHH_AbilityInfo[FHH_State.RealmPlayer] = { };
        end

        for i=1, numCrafts do
                local craftName, craftSubSpellName, _, _, _, _, requiredLevel = GetCraftInfo(i);
                local _, _, rankNum = string.find(craftSubSpellName, "(%d)");
                if (rankNum and tonumber(rankNum)) then
                        rankNum = tonumber(rankNum);
                end
                local craftIcon = GetCraftIcon(i);
                if (craftIcon) then 
                        craftIcon = string.gsub(craftIcon, "^Interface\\Icons\\", "");
                end                     
        
                local spellID = FHH_SpellIDforIcon(craftIcon, craftName);
                local nameSpellID = FHH_SpellIDforName(craftName);
                if (spellID and nameSpellID and spellID ~= nameSpellID) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.SpellIDAliases == nil) then
                                FHH_NewInfo.SpellIDAliases = {};
                        end
                        FHH_NewInfo.SpellIDAliases[nameSpellID] = spellID;
                end
        
                if (FHH_AbilityInfo[FHH_State.RealmPlayer][spellID] == nil) then
                        FHH_AbilityInfo[FHH_State.RealmPlayer][spellID] = {};
                end                     
                if (GFWTable.IndexOf(FHH_AbilityInfo[FHH_State.RealmPlayer][spellID], rankNum) == 0) then
                        table.insert(FHH_AbilityInfo[FHH_State.RealmPlayer][spellID], rankNum)
                end;
        
                if ( requiredLevel and requiredLevel > 0 ) then
                        FHH_RecordNewRequiredLevel(spellID, tonumber(rankNum), requiredLevel, true);
                end
        end
        FHH_ProcessAliases();
end

function FHH_Find(spellID, rankNum)
        local niceSpellName = FHH_SpellIDsToNames[spellID];
        if (niceSpellName == nil and FHH_NewInfo and FHH_NewInfo.SpellIDsToNames and FHH_NewInfo.SpellIDsToNames[spellID]) then
                niceSpellName = FHH_NewInfo.SpellIDsToNames[spellID];
        end
        
        local spellInfo = FHH_SpellInfo[spellID];
        local newSpellInfo;
        if (FHH_NewInfo and FHH_NewInfo.SpellInfo) then
                newSpellInfo = FHH_NewInfo.SpellInfo[spellID];
        end
        if (spellInfo == nil or (type(spellInfo) == "table" and GFWTable.Count(spellInfo) == 0)) then
                if (newSpellInfo == nil or GFWTable.Count(newSpellInfo) == 0) then
                        GFWUtils.Print("No info available for ".. GFWUtils.Hilite(niceSpellName)..".");
                        return;
                else
                        spellInfo = newSpellInfo;
                end
        end
        local rankTable = FHH_RequiredLevel[spellID];
        local newRankTable;
        if (FHH_NewInfo and FHH_NewInfo.RequiredLevel) then
                newRankTable = FHH_NewInfo.RequiredLevel[spellID];
        end
        if (rankTable == nil or GFWTable.Count(rankTable) == 0) then
                if (newRankTable == nil or GFWTable.Count(newRankTable) == 0) then
                        GFWUtils.Print(GFWUtils.Red("Hunter's Helper "..FHH_VERSION.." error:").." found "..GFWUtils.Hilite(niceSpellName).." but can't find rank info. Please report to gazmik@fizzwidget.com");
                        return;
                else
                        rankTable = newRankTable;
                end
        end
        
        rankNum = tonumber(rankNum);
        if (rankNum) then
                if not (rankTable[rankNum]) then
                        GFWUtils.Print(GFWUtils.Hilite(niceSpellName).." is not known to have a rank "..GFWUtils.Hilite(rankNum)..".");
                        return;
                end
                
                -- report minimum pet level for ability
                local minLevel = rankTable[rankNum];
                local petLevel = 60;
                if (UnitExists("pet")) then
                        petLevel = tonumber(UnitLevel("pet"));
                end
                if (minLevel == nil) then
                        minLevel = newRankTable[rankNum];
                end
                if (minLevel == nil) then
                        GFWUtils.Print(GFWUtils.Red("Hunter's Helper "..FHH_VERSION.." error:").." can't find required level for "..GFWUtils.Hilite(niceSpellName.." "..rankNum)..". Please report to gazmik@fizzwidget.com");
                else
                        if (type(minLevel) == "string") then
                                GFWUtils.Print(GFWUtils.Hilite(niceSpellName.." "..rankNum).." requires at least pet level "..GFWUtils.Hilite(minLevel)..". (Assumed because it was found on a beast of this level that you tamed. Open you Beast Training window and Hunter's Helper can collect more accurate information.)");
                        elseif (petLevel >= minLevel) then
                                GFWUtils.Print(GFWUtils.Hilite(niceSpellName.." "..rankNum).." requires pet level "..GFWUtils.Hilite(minLevel)..".");
                        else
                                GFWUtils.Print(GFWUtils.Hilite(niceSpellName.." "..rankNum).." requires pet level "..GFWUtils.Red(minLevel)..".");
                        end
                end
        else
                local knownRanks = {};
                for rankNum in rankTable do
                        table.insert(knownRanks, rankNum);
                end
                local newRanks = {};
                for rankNum in (newRankTable or {}) do
                        table.insert(newRanks, rankNum);
                end
                local allRanks = GFWTable.Merge(knownRanks, newRanks);
                GFWUtils.Print("Ranks known about for "..GFWUtils.Hilite(niceSpellName)..": "..table.concat(allRanks, " "));
                if (type(spellInfo) ~= "string") then
                        GFWUtils.Print("Type "..GFWUtils.Hilite("/hh find "..spellID).." and a number to get info about that rank.");
                end
        end

        -- report available creature families
        local families = FHH_LearnableBy[spellID];
        if (type(families) == "table" and FHH_NewInfo and FHH_NewInfo.LearnableBy and FHH_NewInfo.LearnableBy[spellID]) then
                families = GFWTable.Merge(families, FHH_NewInfo.LearnableBy[spellID]);
                if (table.getn(GFWTable.Diff(families, FHH_AllFamilies)) == 0 ) then
                        families = FHH_ALL_FAMILIES;
                end
        end
        if (families or (type(families) == "table" and table.getn(families) == 0)) then
                if (type(families) == "string") then
                        GFWUtils.Print(GFWUtils.Hilite(niceSpellName).." is learnable by "..GFWUtils.Hilite(families)..".");
                else
                        local listText = table.concat(families, ", ");
                        GFWUtils.Print(GFWUtils.Hilite(niceSpellName).." is learnable by: "..GFWUtils.Hilite(listText)..".");
                end
        end

        -- case 1: first levels of Growl are innate
        if (spellID == "growl" and rankNum and rankNum <= 2) then
                GFWUtils.Print("You should already know "..GFWUtils.Hilite(niceSpellName.." "..rankNum).." if you've learned Beast Training.");
                return;
        end
        
        -- case 2: spells taught by trainers, for which rank doesn't matter
        if (type(spellInfo) == "string") then
                local spellSummary = niceSpellName;
                if (rankNum) then
                        spellSummary = spellSummary.." "..rankNum;
                end
                GFWUtils.Print(GFWUtils.Hilite(spellSummary).." is learned from "..spellInfo..".");
                return;
        end
        
        if (rankNum == nil) then return; end
        
        --case 3: lookup by spell and rank, report by zone (sanity check first)
        local spellRankInfo = spellInfo[rankNum];
        local maxZones = MAX_REPORTED_ZONES;
        if (cmd == "findall") then
                maxZones = 100; -- arbitrarily high so we find everything.
        end
        if (spellRankInfo == nil ) then
                GFWUtils.Print("Hunter's Helper doesn't know about any creatures with "..GFWUtils.Hilite(niceSpellName.." "..rankNum)..".");
                return;
        end
        GFWUtils.Print(GFWUtils.Hilite(niceSpellName.." "..rankNum).." can be learned from:");
        local numReportedZones = 0;
        local zoneName = GFWZones.UnlocalizedZone(GetRealZoneText());
        local critterList = {};
        if (spellRankInfo[zoneName] and table.getn(spellRankInfo[zoneName]) > 0) then
                critterList = GFWTable.Merge(critterList, spellRankInfo[zoneName]);
        end
        if (FHH_NewInfo and FHH_NewInfo.SpellInfo and FHH_NewInfo.SpellInfo[spellID] and FHH_NewInfo.SpellInfo[spellID][rankNum] and FHH_NewInfo.SpellInfo[spellID][rankNum][zoneName]) then
                critterList = GFWTable.Merge(critterList, FHH_NewInfo.SpellInfo[spellID][rankNum][zoneName]);
        end
        if (table.getn(critterList) > 0) then
                GFWUtils.Print(GFWZones.LocalizedZone(zoneName)..": "..GFWUtils.Hilite(FHH_CreatureListString(critterList)));
                numReportedZones = numReportedZones + 1;
        end
        
        local zoneConnections = GFWZones.ConnectionsForZone(zoneName);
        
        if (zoneConnections == nil) then
                -- player is in an unknown zone; instead of doing nothing, let's pick a known zone to start searching from.
                local _, race = UnitRace("player");
                if (race == "Night Elf") then
                        zoneName = "Teldrassil";
                elseif (race == "Dwarf") then
                        zoneName = "Dun Morogh";
                elseif (race == "Gnome") then
                        zoneName = "Dun Morogh";
                elseif (race == "Human") then
                        zoneName = "Elwynn Forest";
                elseif (race == "Tauren") then
                        zoneName = "Mulgore";
                elseif (race == "Orc") then
                        zoneName = "Durotar";
                elseif (race == "Troll") then
                        zoneName = "Durotar";
                elseif (race == "Scourge") then
                        zoneName = "Tirisfal Glades";
                else
                        -- unlikely, but in case we can't parse the race name...
                        local faction = UnitFactionGroup("player");
                        if (faction == "Alliance") then
                                zoneName = "Ironforge";
                        elseif (faction == "Horde") then
                                zoneName = "Orgrimmar";
                        else
                                -- on the off chance we can't even parse a major-faction name...
                                zoneName = "Stranglethorn Vale";
                        end
                end
                zoneConnections = GFWZones.ConnectionsForZone(zoneName);
        end
        
        for _, zones in zoneConnections do
                for _, zoneName in zones do
                        local critterList = {};
                        if (spellRankInfo[zoneName] and table.getn(spellRankInfo[zoneName]) > 0) then
                                critterList = GFWTable.Merge(critterList, spellRankInfo[zoneName]);
                        end
                        if (FHH_NewInfo and FHH_NewInfo.SpellInfo and FHH_NewInfo.SpellInfo[spellID] and FHH_NewInfo.SpellInfo[spellID][rankNum] and FHH_NewInfo.SpellInfo[spellID][rankNum][zoneName]) then
                                critterList = GFWTable.Merge(critterList, FHH_NewInfo.SpellInfo[spellID][rankNum][zoneName]);
                        end
                        if (table.getn(critterList) > 0) then
                                GFWUtils.Print(GFWZones.LocalizedZone(zoneName)..": "..GFWUtils.Hilite(FHH_CreatureListString(critterList)));
                                numReportedZones = numReportedZones + 1;
                                if (numReportedZones >= maxZones) then return; end
                        end
                end
        end
        
        if (numReportedZones == 0) then
                -- if we get here, we think we know about the spell but can't find beasts with it in our table. this shouldn't happen.
                GFWUtils.Print(GFWUtils.Red("Hunter's Helper "..FHH_VERSION.." error:").." got spell info for "..GFWUtils.Hilite(niceSpellName.." "..rankNum).." but no zone info. Please report to gazmik@fizzwidget.com.");
        end
end

function FHH_CreatureListString(critterList)
        local listString = ""
        for _, name in critterList do
                local info = FHH_BeastLevels[name];
                if (info == nil and FHH_NewInfo and FHH_NewInfo.BeastLevels) then
                        info = FHH_NewInfo.BeastLevels[name];
                end
                if (info == nil) then
                        listString = listString..", ";
                else
                        local unlocalizedName = FHH_Localized[name];
                        if (unlocalizedName) then
                                name = unlocalizedName;
                        end
                        listString = listString .. name .. " ";
                        local myLevel = UnitLevel("player");
                        local minLevel = info.min;
                        local maxLevel = info.max;
                        if (info.min > UnitLevel("player")) then
                                minLevel = RED_FONT_COLOR_CODE..info.min..FONT_COLOR_CODE_CLOSE;
                        end
                        if (info.max and info.max > UnitLevel("player")) then
                                maxLevel = RED_FONT_COLOR_CODE..info.max..FONT_COLOR_CODE_CLOSE;
                        end
                        if (info.min == info.max or info.max == nil) then                       
                                listString = listString.."("..minLevel;
                        else
                                listString = listString.."("..minLevel.."-"..maxLevel;
                        end
                        if (info.type == nil) then
                                listString = listString.."), ";
                        else
                                listString = listString.." "..info.type.."), ";
                        end                             
                end
        end
        listString = string.gsub(listString, ", $", "");
        return listString;
end

function FHH_HasTameEffect(unit)

        local i = 1;
        local buff;
        buff = UnitBuff(unit, i);
        while buff do
                if ( string.find(buff, "Ability_Hunter_BeastTaming") ) then
                        return true;
                end
                i = i + 1;
                buff = UnitBuff(unit, i);
        end
        return false;

end

function FHH_SpellIDforName(spellName)
        local spellID = FHH_SpellNamesToIDs[spellName];
        if (spellID == nil and FHH_NewInfo and FHH_NewInfo.SpellNamesToIDs) then
                spellID = FHH_NewInfo.SpellNamesToIDs[spellName];
        end
        return spellID;
end

function FHH_SpellIDforIcon(spellIcon, spellName)
        local spellID = FHH_SpellIcons[spellIcon];
        if (spellID == nil and FHH_NewInfo and FHH_NewInfo.SpellIcons) then
                spellID = FHH_NewInfo.SpellIcons[spellIcon];
        end
        if (spellID == nil) then
                spellID = FHH_SpellIDforName(spellName);
        end     
        if (spellID == nil) then
                spellID = FHH_RecordNewSpellIcon(spellIcon, spellName);
        end
        return spellID;
end

function FHH_CheckPetSpells()
        local currentPetSpells = { };
        local i = 1;
        local spellName, spellRank = GetSpellName(i, BOOKTYPE_PET);
        local spellIcon = GetSpellTexture(i, BOOKTYPE_PET);
        while spellName do
                local _, _, rankNum = string.find(spellRank, "(%d+)");
                if (spellIcon) then 
                        spellIcon = string.gsub(spellIcon, "^Interface\\Icons\\", "");
                end                     
                local spellID = FHH_SpellIDforIcon(spellIcon, spellName);
                local nameSpellID = FHH_SpellIDforName(spellName);
                if (spellID and nameSpellID and spellID ~= nameSpellID) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.SpellIDAliases == nil) then
                                FHH_NewInfo.SpellIDAliases = {};
                        end
                        FHH_NewInfo.SpellIDAliases[nameSpellID] = spellID;
                end

                currentPetSpells[spellID] = tonumber(rankNum);
                i = i + 1;
                spellName, spellRank = GetSpellName(i, BOOKTYPE_PET);
                spellIcon = GetSpellTexture(i, BOOKTYPE_PET);
        end
        
        if (GFWTable.Count(currentPetSpells) > 0) then
                FHH_ProcessAliases();
                FHH_CheckSpellTables(FHH_State.TamingCritter, currentPetSpells);
        else
                --GFWUtils.Print("pet has no spells");
        end
end

function FHH_SpellDescription(spellID, rankNum)
        local niceSpellName = FHH_SpellIDsToNames[spellID];
        if (niceSpellName == nil and FHH_NewInfo and FHH_NewInfo.SpellIDsToNames and FHH_NewInfo.SpellIDsToNames[spellID]) then
                niceSpellName = FHH_NewInfo.SpellIDsToNames[spellID];
        end
        if (niceSpellName == nil) then
                niceSpellName = spellID;
        end
        return niceSpellName.." "..rankNum;
end

function FHH_SpellDescriptions(spellList)
        local descriptions = {};
        for spellID, rankNum in spellList do
                table.insert(descriptions, FHH_SpellDescription(spellID, rankNum));
        end
        return descriptions;
end

function FHH_SpellDescripionList(spellList)
        return table.concat(FHH_SpellDescriptions(spellList), ", ");
end

function FHH_CheckSpellTables(critter, spellList, level, family)
        
        if ( spellList == nil or GFWTable.Count(spellList) == 0 ) then return; end

        -- process any recently learned spellID aliases so we record data correctly.
        local newSpellList = {};
        local changed = false;
        for spellID, rankNum in spellList do
                if (FHH_NewInfo and FHH_NewInfo.SpellIDAliases and FHH_NewInfo.SpellIDAliases[spellID]) then
                        spellID = FHH_NewInfo.SpellIDAliases[spellID];
                        changed = true;
                end
                newSpellList[spellID] = rankNum;
        end
        if (changed) then
                spellList = newSpellList;
        end     

        if (level == nil) then
                level = UnitLevel("pet");
        end
        if (family == nil) then
                family = UnitCreatureFamily("pet");
        end
        
        if ( FHH_BeastInfo[critter] ) then
        
                -- record any spells the critter has that our built-in table doesn't know about 
                local unknownPetSpells = { };
                for spellID, rankNum in spellList do
                        if ( FHH_BeastInfo[critter][spellID] == nil ) then
                                unknownPetSpells[spellID] = rankNum;
                        end
                end
                if ( GFWTable.Count(unknownPetSpells) > 0 ) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.BeastInfo == nil) then
                                FHH_NewInfo.BeastInfo = {};
                        end
                        FHH_NewInfo.BeastInfo[critter] = spellList; -- we want to remember the entire current spells list
                end
                
                -- record any spells our built-in table thinks the critter has, but the critter actually doesn't
                local wrongPetSpells = { };
                for spellID, rankNum in FHH_BeastInfo[critter] do
                        if ( spellList[spellID] ~= rankNum ) then
                                wrongPetSpells[spellID] = rankNum;
                        end
                end
                if ( GFWTable.Count(wrongPetSpells) > 0 ) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.BadBeastInfo == nil) then
                                FHH_NewInfo.BadBeastInfo = {};
                        end
                        FHH_NewInfo.BadBeastInfo[critter] = wrongPetSpells;
                end
                
                if (FHH_NewInfo and (( FHH_NewInfo.BeastInfo and FHH_NewInfo.BeastInfo[critter]) or (FHH_NewInfo.BadBeastInfo and FHH_NewInfo.BadBeastInfo[critter]))) then
                        local details = "(expected "..FHH_SpellDescripionList(FHH_BeastInfo[critter]).."; found "..FHH_SpellDescripionList(spellList)..").";
                        GFWUtils.PrintOnce("Hunter's Helper "..FHH_VERSION.." has incorrect data on "..GFWUtils.Hilite(critter.." "..details).." Please submit a correction to gazmik@fizzwidget.com.)", 60);
                end
                
        else
        
                -- this pet is entirely new to our list
                if (FHH_NewInfo == nil) then
                        FHH_NewInfo = {};
                end
                if (FHH_NewInfo.BeastInfo == nil) then
                        FHH_NewInfo.BeastInfo = {};
                end
                FHH_NewInfo.BeastInfo[critter] = spellList;
                FHH_CheckBeastLevel(critter, level, FHH_State.TamingType);
                
                local details = "(found "..FHH_SpellDescripionList(spellList).." in "..GetRealZoneText()..").";
                GFWUtils.PrintOnce("Hunter's Helper "..FHH_VERSION.." has no data on "..GFWUtils.Hilite(critter.." "..details).." Please submit a correction to gazmik@fizzwidget.com.)", 60);

        end
        
        for spellID, rankNum in spellList do
                FHH_RecordNewSpellInfo(spellID, rankNum, critter);
                FHH_RecordNewRequiredFamily(spellID, family);
                FHH_RecordNewRequiredLevel(spellID, rankNum, level);
        end

end

function FHH_RecordNewSpellInfo(spellID, rankNum, critter)
        if (FHH_SpellInfo[spellID] and FHH_SpellInfo[spellID][rankNum]) then
                for zoneName, beastsTable in FHH_SpellInfo[spellID][rankNum] do
                        for _, aBeast in beastsTable do
                                if (aBeast == critter) then
                                        return; -- we've already recorded this in our static data
                                end
                        end
                end
        end
        
        if (FHH_NewInfo == nil) then
                FHH_NewInfo = {};
        end
        if (FHH_NewInfo.SpellInfo == nil) then
                FHH_NewInfo.SpellInfo = {};
        end
        if (FHH_NewInfo.SpellInfo[spellID] == nil) then
                FHH_NewInfo.SpellInfo[spellID] = {};
        end
        if (FHH_NewInfo.SpellInfo[spellID][rankNum] == nil) then
                FHH_NewInfo.SpellInfo[spellID][rankNum] = {};
        end
        local currentZone = GFWZones.UnlocalizedZone(GetRealZoneText());
        if (FHH_NewInfo.SpellInfo[spellID][rankNum][currentZone] == nil) then
                FHH_NewInfo.SpellInfo[spellID][rankNum][currentZone] = {};
        end
        if (not GFWTable.KeyOf(FHH_NewInfo.SpellInfo[spellID][rankNum][currentZone], critter)) then
                table.insert(FHH_NewInfo.SpellInfo[spellID][rankNum][currentZone], critter);
        end
end

function FHH_RecordNewRequiredFamily(spellID, family)
        if (FHH_LearnableBy[spellID] and GFWTable.KeyOf(FHH_LearnableBy[spellID], family)) then
                return; -- we've already recorded this in our static data
        end
        
        if (FHH_NewInfo == nil) then
                FHH_NewInfo = {};
        end
        if (FHH_NewInfo.LearnableBy == nil) then
                FHH_NewInfo.LearnableBy = {};
        end
        if (FHH_NewInfo.LearnableBy[spellID] == nil) then
                FHH_NewInfo.LearnableBy[spellID] = {};
        end
        if (not GFWTable.KeyOf(FHH_NewInfo.LearnableBy[spellID], family)) then
                table.insert(FHH_NewInfo.LearnableBy[spellID], family);
        end
end

function FHH_RecordNewRequiredLevel(spellID, rankNum, level, verified)  
        if (FHH_RequiredLevel[spellID] and FHH_RequiredLevel[spellID][rankNum]) then
                return; -- we've already recorded this in our static data
        end
        
        if (FHH_NewInfo == nil) then
                FHH_NewInfo = {};
        end
        if (FHH_NewInfo.RequiredLevel == nil) then
                FHH_NewInfo.RequiredLevel = {};
        end
        if (FHH_NewInfo.RequiredLevel[spellID] == nil) then
                FHH_NewInfo.RequiredLevel[spellID] = {};
        end
        if (verified) then
                FHH_NewInfo.RequiredLevel[spellID][rankNum] = level;
        elseif (FHH_NewInfo.RequiredLevel[spellID][rankNum] == nil) then
                FHH_NewInfo.RequiredLevel[spellID][rankNum] = tostring(level);
        else
                local existingRank = FHH_NewInfo.RequiredLevel[spellID][rankNum];
                if (type(existingRank) == "string") then
                        -- we don't have a certain answer yet, we'll use what we just got to refine it
                        FHH_NewInfo.RequiredLevel[spellID][rankNum] = tostring(math.min(level, tonumber(existingRank)));
                end
        end
end

function FHH_CheckBeastLevel(creepName, creepLevel, creepType)
        if (creepLevel < 1) then
                return; -- UnitLevel sometimes returns -1 for common mobs (maybe a WDB cache thing) so we toss nonsensical values.
        end

        if (FHH_NewInfo and FHH_NewInfo.BeastLevels and FHH_NewInfo.BeastLevels[creepName]) then
                FHH_NewInfo.BeastLevels[creepName].min = math.min(FHH_NewInfo.BeastLevels[creepName].min, creepLevel);
                FHH_NewInfo.BeastLevels[creepName].max = math.max(FHH_NewInfo.BeastLevels[creepName].max, creepLevel);
                if (FHH_NewInfo.BeastLevels[creepName].type and creepType ~= "normal") then
                        FHH_NewInfo.BeastLevels[creepName].type = creepType;
                end
        elseif (FHH_BeastLevels[creepName]) then
                if (creepLevel < FHH_BeastLevels[creepName].min or (FHH_BeastLevels[creepName].max and creepLevel > FHH_BeastLevels[creepName].max)) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.BeastLevels == nil) then
                                FHH_NewInfo.BeastLevels = {};
                        end
                        FHH_NewInfo.BeastLevels[creepName] = {};
                        FHH_NewInfo.BeastLevels[creepName].min = math.min(FHH_BeastLevels[creepName].min, creepLevel);
                        FHH_NewInfo.BeastLevels[creepName].max = math.max(FHH_BeastLevels[creepName].max or FHH_BeastLevels[creepName].min, creepLevel);
                end
                if (creepType ~= "normal" and creepType ~= FHH_BeastLevels[creepName].type) then
                        if (FHH_NewInfo == nil) then
                                FHH_NewInfo = {};
                        end
                        if (FHH_NewInfo.BeastLevels == nil) then
                                FHH_NewInfo.BeastLevels = {};
                        end
                        if (FHH_NewInfo.BeastLevels[creepName] == nil) then
                                FHH_NewInfo.BeastLevels[creepName] = {};
                        end
                        FHH_NewInfo.BeastLevels[creepName].min = math.min(FHH_BeastLevels[creepName].min, creepLevel);
                        FHH_NewInfo.BeastLevels[creepName].max = math.max(FHH_BeastLevels[creepName].max or FHH_BeastLevels[creepName].min, creepLevel);
                        FHH_NewInfo.BeastLevels[creepName].type = creepType;
                end
        end
end

function FHH_RecordNewSpellID(spellName)
        -- we have a new spell on our hands; we'll use its lowercase name as a key for now.
        spellID = string.lower(spellName);
        if (FHH_NewInfo == nil) then
                FHH_NewInfo = {};
        end
        if (FHH_NewInfo.SpellNamesToIDs == nil) then
                FHH_NewInfo.SpellNamesToIDs = {};
        end
        if (FHH_NewInfo.SpellIDsToNames == nil) then
                FHH_NewInfo.SpellIDsToNames = {};
        end
        FHH_NewInfo.SpellNamesToIDs[spellName] = spellID;
        FHH_NewInfo.SpellIDsToNames[spellID] = spellName;
        return spellID;
end

function FHH_RecordNewSpellIcon(spellIcon, spellName)
        spellID = FHH_RecordNewSpellID(spellName);
        if (FHH_NewInfo == nil) then
                FHH_NewInfo = {};
        end
        if (FHH_NewInfo.SpellIcons == nil) then
                FHH_NewInfo.SpellIcons = {};
        end
        FHH_NewInfo.SpellIcons[spellIcon] = spellID;
        return spellID;
end

function FHH_ProcessAliases()
        if (FHH_NewInfo and FHH_NewInfo.SpellIDAliases) then
                for oldID, newID in FHH_NewInfo.SpellIDAliases do
                        
                        if (FHH_NewInfo.SpellNamesToIDs) then
                                local newNamesToIDs = {};
                                local changed = false;
                                for name, id in FHH_NewInfo.SpellNamesToIDs do
                                        if (id == oldID) then
                                                newNamesToIDs[name] = newID;
                                                changed = true;
                                        else
                                                newNamesToIDs[name] = id;
                                        end
                                end
                                if (changed) then
                                        FHH_NewInfo.SpellNamesToIDs = newNamesToIDs;
                                end
                        end

                        if (FHH_NewInfo.BeastInfo) then
                                for beast, spellList in FHH_NewInfo.BeastInfo do
                                        if (spellList[oldID]) then
                                                spellList[newID] = spellList[oldID];
                                                spellList[oldID] = nil;
                                        end
                                end
                        end

                        if (FHH_NewInfo.BadBeastInfo) then
                                for beast, spellList in FHH_NewInfo.BadBeastInfo do
                                        if (spellList[oldID]) then
                                                spellList[newID] = spellList[oldID];
                                                spellList[oldID] = nil;
                                        end
                                end
                        end
                        
                        if (FHH_AbilityInfo) then
                                for realmPlayer, abilityTable in FHH_AbilityInfo do
                                        if (abilityTable[oldID]) then
                                                abilityTable[newID] = abilityTable[oldID];
                                                abilityTable[oldID] = nil;
                                        end
                                end
                        end

                        if (FHH_NewInfo.SpellIDsToNames and FHH_NewInfo.SpellIDsToNames[oldID]) then
                                FHH_NewInfo.SpellIDsToNames[newID] = FHH_NewInfo.SpellIDsToNames[oldID];
                                FHH_NewInfo.SpellIDsToNames[oldID] = nil;
                        end
                                                
                        if (FHH_NewInfo.RequiredLevel and FHH_NewInfo.RequiredLevel[oldID]) then
                                FHH_NewInfo.RequiredLevel[newID] = FHH_NewInfo.RequiredLevel[oldID];
                                FHH_NewInfo.RequiredLevel[oldID] = nil;                         
                        end

                        if (FHH_NewInfo.LearnableBy and FHH_NewInfo.LearnableBy[oldID]) then
                                FHH_NewInfo.LearnableBy[newID] = FHH_NewInfo.LearnableBy[oldID];
                                FHH_NewInfo.LearnableBy[oldID] = nil;                           
                        end

                        if (FHH_NewInfo.SpellInfo and FHH_NewInfo.SpellInfo[oldID]) then
                                FHH_NewInfo.SpellInfo[newID] = FHH_NewInfo.SpellInfo[oldID];
                                FHH_NewInfo.SpellInfo[oldID] = nil;                             
                        end
                end
        end
end