------------------------------------------------------ -- Linkerator.lua -- Link scanning code based on LootLink 1.9. Thanks Telo! ------------------------------------------------------ FLT_VERSION = "11000.2"; ------------------------------------------------------ FLT_ItemLinks = { }; local ChatMessageTypes = { "CHAT_MSG_SYSTEM", "CHAT_MSG_SAY", "CHAT_MSG_TEXT_EMOTE", "CHAT_MSG_YELL", "CHAT_MSG_WHISPER", "CHAT_MSG_PARTY", "CHAT_MSG_GUILD", "CHAT_MSG_OFFICER", "CHAT_MSG_CHANNEL", "CHAT_MSG_RAID", "CHAT_MSG_LOOT", }; local INVENTORY_SLOT_LIST = { { name = "HeadSlot" }, { name = "NeckSlot" }, { name = "ShoulderSlot" }, { name = "BackSlot" }, { name = "ChestSlot" }, { name = "ShirtSlot" }, { name = "TabardSlot" }, { name = "WristSlot" }, { name = "HandsSlot" }, { name = "WaistSlot" }, { name = "LegsSlot" }, { name = "FeetSlot" }, { name = "Finger0Slot" }, { name = "Finger1Slot" }, { name = "Trinket0Slot" }, { name = "Trinket1Slot" }, { name = "MainHandSlot" }, { name = "SecondaryHandSlot" }, { name = "RangedSlot" }, }; -- Anti-freeze code borrowed from ReagentInfo (in turn, from Quest-I-On): -- keeps WoW from locking up if we try to scan the tradeskill window too fast. FLT_TradeSkillLock = { }; FLT_TradeSkillLock.NeedScan = false; FLT_TradeSkillLock.Locked = false; FLT_TradeSkillLock.EventTimer = 0; FLT_TradeSkillLock.EventCooldown = 0; FLT_TradeSkillLock.EventCooldownTime = 1; FLT_CraftLock = { }; FLT_CraftLock.NeedScan = false; FLT_CraftLock.Locked = false; FLT_CraftLock.EventTimer = 0; FLT_CraftLock.EventCooldown = 0; FLT_CraftLock.EventCooldownTime = 1; function FLT_OnLoad() --GFWUtils.Debug = 1; -- Register Slash Commands SLASH_FLT1 = "/linkerator"; SLASH_FLT2 = "/link"; SlashCmdList["FLT"] = function(msg) FLT_ChatCommandHandler(msg); end for index, value in ChatMessageTypes do this:RegisterEvent(value); end for index = 1, getn(INVENTORY_SLOT_LIST), 1 do INVENTORY_SLOT_LIST[index].id = GetInventorySlotInfo(INVENTORY_SLOT_LIST[index].name); end this:RegisterEvent("PLAYER_ENTERING_WORLD"); this:RegisterEvent("PLAYER_LEAVING_WORLD"); this:RegisterEvent("PLAYER_TARGET_CHANGED"); this:RegisterEvent("BANKFRAME_OPENED"); this:RegisterEvent("MERCHANT_SHOW"); this:RegisterEvent("TRADE_SKILL_SHOW"); this:RegisterEvent("TRADE_SKILL_UPDATE"); this:RegisterEvent("CRAFT_SHOW"); this:RegisterEvent("CRAFT_UPDATE"); this:RegisterEvent("QUEST_COMPLETE"); this:RegisterEvent("QUEST_DETAIL"); this:RegisterEvent("QUEST_FINISHED"); this:RegisterEvent("QUEST_PROGRESS"); this:RegisterEvent("QUEST_GREETING"); this:RegisterEvent("AUCTION_ITEM_LIST_UPDATE"); GFWUtils.Print("Fizzwidget Linkerator "..FLT_VERSION.." initialized!"); end function FLT_OnUpdate(elapsed) -- If it's been more than a second since our last tradeskill update, -- we can allow the event to process again. FLT_TradeSkillLock.EventTimer = FLT_TradeSkillLock.EventTimer + elapsed; if (FLT_TradeSkillLock.Locked) then FLT_TradeSkillLock.EventCooldown = FLT_TradeSkillLock.EventCooldown + elapsed; if (FLT_TradeSkillLock.EventCooldown > FLT_TradeSkillLock.EventCooldownTime) then FLT_TradeSkillLock.EventCooldown = 0; FLT_TradeSkillLock.Locked = false; end end FLT_CraftLock.EventTimer = FLT_CraftLock.EventTimer + elapsed; if (FLT_CraftLock.Locked) then FLT_CraftLock.EventCooldown = FLT_CraftLock.EventCooldown + elapsed; if (FLT_CraftLock.EventCooldown > FLT_CraftLock.EventCooldownTime) then FLT_CraftLock.EventCooldown = 0; FLT_CraftLock.Locked = false; end end if (FLT_TradeSkillLock.NeedScan) then FLT_TradeSkillLock.NeedScan = false; FLT_ScanTradeSkill(); end if (FLT_CraftLock.NeedScan) then FLT_CraftLock.NeedScan = false; FLT_ScanCraft(); end end function FLT_OnEvent(event) if( event == "PLAYER_TARGET_CHANGED" ) then if( UnitIsUnit("target", "player") ) then return; elseif( UnitIsPlayer("target") ) then FLT_Inspect("target"); end elseif( event == "PLAYER_ENTERING_WORLD" ) then FLT_ScanInventory(); FLT_Inspect("player"); this:RegisterEvent("UNIT_INVENTORY_CHANGED"); elseif( event == "PLAYER_LEAVING_WORLD" ) then this:UnregisterEvent("UNIT_INVENTORY_CHANGED"); elseif( event == "UNIT_INVENTORY_CHANGED" ) then if( arg1 == "player" ) then FLT_ScanInventory(); FLT_Inspect("player"); end elseif( event == "MERCHANT_SHOW" ) then for i=1, GetMerchantNumItems() do local link = GetMerchantItemLink(i); if ( link ) then FLT_ProcessLinks(link); end end elseif( event == "BANKFRAME_OPENED" ) then FLT_ScanBank(); elseif( event == "AUCTION_ITEM_LIST_UPDATE" ) then FLT_ScanAuction(); elseif ( event == "QUEST_COMPLETE" or event == "QUEST_DETAIL" or event == "QUEST_FINISHED" or event == "QUEST_PROGRESS" or event == "QUEST_GREETING") then FLT_ScanQuestgiver(); elseif ( event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_UPDATE" ) then FLT_ScanTradeSkill(); elseif ( event == "CRAFT_SHOW" or event == "CRAFT_UPDATE" ) then FLT_ScanCraft(); elseif (event == "CHAT_MSG_CHANNEL") then --DevTools_Dump({event=event, args={arg1=arg1,arg2=arg2,arg3=arg3,arg4=arg4,arg5=arg5,arg6=arg6,arg7=arg7,arg8=arg8,arg9=arg9,}}); if (FLT_Debug) then debugprofilestart(); end local gotLink = FLT_ProcessLinks(arg1); if (FLT_Debug) then local parseTime = debugprofilestop(); if (gotLink) then FLT_MaxFoundTime = math.max((FLT_MaxFoundTime or 0), parseTime); else FLT_MaxNotFoundTime = math.max((FLT_MaxNotFoundTime or 0), parseTime); end end else FLT_ProcessLinks(arg1); end end function FLT_ChatCommandHandler(msg) -- Print Help if ( msg == "help" ) or ( msg == "" ) then GFWUtils.Print("Fizzwidget Linkerator "..FLT_VERSION..":"); GFWUtils.Print("/linkerator (or /link) "); GFWUtils.Print("- "..GFWUtils.Hilite("help").." - Print this helplist."); GFWUtils.Print("- "..GFWUtils.Hilite("").." - Print a hyperlink to the chat window for an item known by name."); GFWUtils.Print("- "..GFWUtils.Hilite("").." - Print a hyperlink to the chat window for a generic item whose ID number is known."); GFWUtils.Print("- "..GFWUtils.Hilite("").." - Print a hyperlink to the chat window for an item whose complete link code is known."); return; end if (msg == "version") then GFWUtils.Print("Fizzwidget Linkerator "..FLT_VERSION); return; end if (msg == "list") then local totalCount, knownCount = 0, 0; for name, linkInfo in FLT_ItemLinks do if (type(linkInfo) == "string") then totalCount = totalCount + 1; local link = FLT_GetItemLink(linkInfo); if (link) then GFWUtils.Print(link); knownCount = knownCount + 1; else GFWUtils.Print(name.." ("..linkInfo.."; not cached)"); end elseif (type(linkInfo) == "table") then for _, linkID in linkInfo do totalCount = totalCount + 1; local link = FLT_GetItemLink(linkID); if (link) then GFWUtils.Print(link); knownCount = knownCount + 1; else GFWUtils.Print(name.." ("..linkID.."; not cached)"); end end end end GFWUtils.Print(GFWUtils.Hilite(totalCount).." items in history, "..GFWUtils.Hilite(knownCount).." known to client."); return; end if (msg == "count") then local totalCount, knownCount = 0, 0; for name, linkInfo in FLT_ItemLinks do if (type(linkInfo) == "string") then totalCount = totalCount + 1; local link = FLT_GetItemLink(linkInfo); if (link) then knownCount = knownCount + 1; end elseif (type(linkInfo) == "table") then for _, linkID in linkInfo do totalCount = totalCount + 1; local link = FLT_GetItemLink(linkID); if (link) then knownCount = knownCount + 1; end end end end GFWUtils.Print(GFWUtils.Hilite(totalCount).." items in history, "..GFWUtils.Hilite(knownCount).." known to client."); return; end if (FLT_Debug and msg == "time") then GFWUtils.Print(string.format("Max parse time "..GFWUtils.Hilite("%.2f").." ms (no links found)", FLT_MaxNotFoundTime * 1000)); GFWUtils.Print(string.format("Max parse time "..GFWUtils.Hilite("%.2f").." ms (links found)", FLT_MaxFoundTime * 1000)); return; end if (msg == "debug") then if (FLT_Debug) then FLT_Debug = nil; GFWUtils.Print("Linkerator debugging messages off."); else FLT_Debug = 1; GFWUtils.Print("Linkerator debugging messages on."); end return; end if (msg == "import") then local lootLinkCount = 0; if (ItemLinks and type(ItemLinks) == "table") then for itemName, lootLinkInfo in ItemLinks do if (lootLinkInfo.i and type(lootLinkInfo.i) == "string") then local itemLink = "item:"..lootLinkInfo.i; local addedLink = FLT_AddLink(itemName, itemLink); if (addedLink) then lootLinkCount = lootLinkCount + 1; end end end end GFWUtils.Print(GFWUtils.Hilite(lootLinkCount).. " items imported from LootLink."); end local itemLink; if (msg and msg ~= "") then _, _, itemLink = string.find(msg, "(item:%d+:%d+:%d+:%d+)"); end if (tonumber(msg)) then --DevTools_Dump({msg=msg}); local link = FLT_GetItemLink(msg, 1); if (link) then GFWUtils.Print("Item ID "..msg..": "..link); else GFWUtils.Print("Item ID "..msg.." is unknown to this WoW client."); end return; elseif (itemLink) then --DevTools_Dump({msg=msg, itemLink=itemLink}); local link = FLT_GetItemLink(itemLink, 1); if (link) then GFWUtils.Print(itemLink..": "..link); else GFWUtils.Print(itemLink.." is unknown to this WoW client."); end return; elseif (msg and msg ~= "") then local foundLinks = FLT_GetLinkByName(msg, true); if (foundLinks) then if (type(foundLinks) == "string") then if (FLT_Debug) then local _, _, linkInfo = string.find(foundLinks, "(item:%d+:%d+:%d+:%d+)"); GFWUtils.Print(foundLinks.." ("..linkInfo..")"); else GFWUtils.Print(foundLinks); end elseif (type(foundLinks) == "table") then for _, aLink in foundLinks do if (FLT_Debug) then local _, _, linkInfo = string.find(aLink, "(item:%d+:%d+:%d+:%d+)"); GFWUtils.Print(aLink.." ("..linkInfo..")"); else GFWUtils.Print(aLink); end end else GFWUtils.Print(GFWUtils.Red("Linkerator error: ").."Unexpected result from FLT_GetLinkByName()."); end else local foundCount = 0; msg = string.lower(msg); for itemName, linkInfo in FLT_ItemLinks do if (string.find(itemName, msg)) then if (type(linkInfo) == "string") then local link = FLT_GetItemLink(linkInfo); if (link) then if (FLT_Debug) then GFWUtils.Print(link.." ("..linkInfo..")"); else GFWUtils.Print(link); end foundCount = foundCount + 1; end elseif (type(linkInfo) == "table") then for _, aLink in linkInfo do local link = FLT_GetItemLink(aLink); if (link) then if (FLT_Debug) then GFWUtils.Print(link.." ("..aLink..")"); else GFWUtils.Print(link); end foundCount = foundCount + 1; end end end end end if (foundCount > 0) then GFWUtils.Print(GFWUtils.Hilite(foundCount).." links found for '"..GFWUtils.Hilite(msg).."'"); else GFWUtils.Print("Could not find '"..GFWUtils.Hilite(msg).."' in Linkerator's item history."); end end return; end -- If we're this far, we probably have bad input. FDP_ChatCommandHandler("help"); end function FLT_GetItemLink(linkInfo, shouldAdd) local sName, sLink, iQuality, iLevel, sType, sSubType, iCount = GetItemInfo(linkInfo); if (sName) then local _, _, _, color = GetItemQualityColor(iQuality); local linkFormat = "%s|H%s|h[%s]|h|r"; if (shouldAdd) then FLT_AddLink(sName, sLink); -- add it to our name index if we're getting it from another source end return string.format(linkFormat, color, sLink, sName); else return nil; end end function FLT_GetLinkByName(text, returnAll) local _, _, name, property = string.find(text, "(.+)%((.-)%)" ); local linkInfo = FLT_ItemLinks[string.lower(text)]; local allResults = {}; if (linkInfo == nil and name and property) then name = string.lower(string.gsub(name, " +$", "")); property = string.lower(property); linkInfo = FLT_ItemLinks[string.lower(name)]; end if (linkInfo) then if (type(linkInfo) == "string") then local link = FLT_GetItemLink(linkInfo); if (link) then return link; end elseif (type(linkInfo) == "table" and name and property) then for _, itemLink in linkInfo do local _, _, _, _, type, subType, _, equipLoc = GetItemInfo(itemLink); if ((type and string.find(string.lower(type), property)) or (subType and string.find(string.lower(subType), property)) or (getglobal(equipLoc) and string.find(string.lower(getglobal(equipLoc)), property))) then local link = FLT_GetItemLink(itemLink); if (link and returnAll) then table.insert(allResults, link); elseif (link) then return link; end end end for _, itemLink in linkInfo do if (FLT_FindInItemTooltip(property, itemLink)) then if (returnAll) then table.insert(allResults, FLT_GetItemLink(itemLink)); else return FLT_GetItemLink(itemLink); end end end elseif (type(linkInfo) == "table") then for _, itemLink in linkInfo do local link = FLT_GetItemLink(itemLink); if (link and returnAll) then table.insert(allResults, link); elseif (link) then return link; end end else GFWUtils.Print(GFWUtils.Red("Linkerator error: ").."Corrupt table entry for "..GFWUtils.Hilite(name).."."); end elseif (string.find(text, "^(item:%d+:%d+:%d+:%d+)$")) then local link = FLT_GetItemLink(text); if (link) then return link; end elseif (string.find(text, "^#%d+$")) then local link = FLT_GetItemLink(string.sub(text,2)); if (link) then return link; end end if (returnAll and table.getn(allResults) > 0) then return allResults; end end function FLT_ProcessLinks(text) local lastLink; if ( text ) then local link, name; for link, name in string.gfind(text, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[(.-)%]|h|r") do if (link and name and name ~= "") then lastLink = FLT_AddLink(name, link); end end end return lastLink; end function FLT_AddLink(name, link) local cleanLink = string.gsub(link, "item:(%d+):%d+:(%d+):%d+", "item:%1:0:%2:0"); -- strip uniqID, enchant name = string.lower(name); -- so we can do case-insensitive lookups local existingLink = FLT_ItemLinks[name]; if (existingLink and existingLink ~= cleanLink) then if (type(existingLink) == "string") then FLT_ItemLinks[name] = {}; table.insert(FLT_ItemLinks[name], existingLink); table.insert(FLT_ItemLinks[name], cleanLink); if (FLT_Debug) then GFWUtils.Print("Added "..(FLT_GetItemLink(cleanLink) or name).." ("..cleanLink.."); changed to table"); end elseif (type(existingLink) == "table") then if (GFWTable.KeyOf(existingLink, cleanLink) == nil) then FLT_ItemLinks[name] = GFWTable.Merge(existingLink, {cleanLink}); if (FLT_Debug) then GFWUtils.Print("Added "..(FLT_GetItemLink(cleanLink) or name).." ("..cleanLink.."); table count is now "..GFWTable.Count(FLT_ItemLinks[name])); end end else GFWUtils.PrintOnce("Corrupt entry for "..GFWUtils.Hilite(name).."; replacing with "..FLT_GetItemLink(cleanLink)..".", 60); FLT_ItemLinks[name] = cleanLink; end elseif (existingLink == nil) then FLT_ItemLinks[name] = cleanLink; if (FLT_Debug) then GFWUtils.Print("Added "..(FLT_GetItemLink(cleanLink) or name).." ("..cleanLink..")"); end end return cleanLink; end function FLT_InspectSlot(unit, id) local link = GetInventoryItemLink(unit, id); if ( link ) then FLT_ProcessLinks(link); end end function FLT_Inspect(who) local index; for index = 1, getn(INVENTORY_SLOT_LIST), 1 do FLT_InspectSlot(who, INVENTORY_SLOT_LIST[index].id) end end function FLT_ScanTradeSkill() if (not TradeSkillFrame or not TradeSkillFrame:IsVisible() or FLT_TradeSkillLock.Locked) then return; end -- This prevents further update events from being handled if we're already processing one. -- This is done to prevent the game from freezing under certain conditions. FLT_TradeSkillLock.Locked = true; local skillLineName, skillLineRank, skillLineMaxRank = GetTradeSkillLine(); if not (skillLineName) then FLT_TradeSkillLock.NeedScan = true; return; -- apparently sometimes we're called too early, this is nil, and all hell breaks loose. end for id = 1, GetNumTradeSkills() do local skillName, skillType, numAvailable, isExpanded = GetTradeSkillInfo(id); if ( skillType ~= "header" ) then local itemLink = GetTradeSkillItemLink(id); if (itemLink == nil) then FLT_TradeSkillLock.NeedScan = true; else FLT_ProcessLinks(itemLink); for i=1, GetTradeSkillNumReagents(id), 1 do local link = GetTradeSkillReagentItemLink(id, i); if (link == nil) then FLT_TradeSkillLock.NeedScan = true; break; else FLT_ProcessLinks(link); end end end end end end function FLT_ScanCraft() if (not CraftFrame or not CraftFrame:IsVisible() or FLT_CraftLock.Locked) then return; end -- This prevents further update events from being handled if we're already processing one. -- This is done to prevent the game from freezing under certain conditions. FLT_CraftLock.Locked = true; -- This is used only for Enchanting local skillLineName, rank, maxRank = GetCraftDisplaySkillLine(); if not (skillLineName) then return; -- Hunters' Beast Training also uses the CraftFrame, but doesn't have a SkillLine. end for id = GetNumCrafts(), 1, -1 do if ( craftType ~= "header" ) then local itemLink = GetCraftItemLink(id); if (itemLink == nil) then FLT_TradeSkillLock.NeedScan = true; else FLT_ProcessLinks(itemLink); for i=1, GetCraftNumReagents(id), 1 do local link = GetCraftReagentItemLink(id, i); if (link == nil) then FLT_CraftLock.NeedScan = true; break; else FLT_ProcessLinks(link); end end end end end end function FLT_ScanQuestgiver() local link; for i = 1, GetNumQuestItems() do link = GetQuestItemLink("required", i); if (link) then FLT_ProcessLinks(link); end end for i = 1, GetNumQuestChoices() do link = GetQuestItemLink("choice", i); if (link) then FLT_ProcessLinks(link); end end for i = 1, GetNumQuestRewards() do link = GetQuestItemLink("reward", i); if (link) then FLT_ProcessLinks(link); end end end function FLT_ScanInventory() local bagid, size, slotid, link; for bagid = 0, 4, 1 do size = GetContainerNumSlots(bagid); if( size ) then for slotid = size, 1, -1 do link = GetContainerItemLink(bagid, slotid); if( link ) then FLT_ProcessLinks(link); end end end end end function FLT_ScanAuction() local numBatchAuctions, totalAuctions = GetNumAuctionItems("list"); local auctionid, link; if( numBatchAuctions > 0 ) then for auctionid = 1, numBatchAuctions do link = GetAuctionItemLink("list", auctionid); if( link ) then FLT_ProcessLinks(link); end end end end function FLT_ScanBank() local index, bagid, size, slotid, link; local lBankBagIDs = { BANK_CONTAINER, 5, 6, 7, 8, 9, 10, }; for index, bagid in lBankBagIDs do size = GetContainerNumSlots(bagid); if( size ) then for slotid = size, 1, -1 do link = GetContainerItemLink(bagid, slotid); if( link ) then FLT_ProcessLinks(link); end end end end end function FLT_LinkifyName(head, text, tail) if (head ~= "|h" and tail ~= "|h") then -- only linkify things text that isn't linked already local link = FLT_GetLinkByName(text); if (link) then return link; end end return head.."["..text.."]"..tail; end function FLT_FindInItemTooltip(text, link) LinkeratorTip:ClearLines(); LinkeratorTip:SetHyperlink(link); for lineNum = 1, LinkeratorTip:NumLines() do local leftText = getglobal("LinkeratorTipTextLeft"..lineNum):GetText(); if (leftText and string.find(string.lower(leftText), text)) then return true; end local rightText = getglobal("LinkeratorTipTextRight"..lineNum):GetText(); if (rightText and string.find(string.lower(rightText), text)) then return true; end end for lineNum = 1, LinkeratorTip:NumLines() do -- for some reason ClearLines alone isn't clearing the right-side text getglobal("LinkeratorTipTextLeft"..lineNum):SetText(nil); getglobal("LinkeratorTipTextRight"..lineNum):SetText(nil); end end function FLT_ParseChatMessage(text) return string.gsub(text, "([|]?[h]?)%[(.-)%]([|]?[h]?)", FLT_LinkifyName); end -- Hooks FLT_Orig_ChatEdit_OnTextChanged = ChatEdit_OnTextChanged; function ChatEdit_OnTextChanged() local text = this:GetText(); if (string.find(text, "^/script") or string.find(text, "^/dump")) then -- don't parse else text = FLT_ParseChatMessage(text); this:SetText(text); end FLT_Orig_ChatEdit_OnTextChanged(this); end FLT_Orig_QuestLog_UpdateQuestDetails = QuestLog_UpdateQuestDetails; function QuestLog_UpdateQuestDetails() for i = 1, GetNumQuestLogChoices() do link = GetQuestLogItemLink("choice", i); if (link) then FLT_ProcessLinks(link); end end for i = 1, GetNumQuestLogRewards() do link = GetQuestLogItemLink("reward", i); if (link) then FLT_ProcessLinks(link); end end FLT_Orig_QuestLog_UpdateQuestDetails(); end