-- Right-click a lockbox in your bag to unlock when you are not in combat -- Game Issue #1: The game prioritizes the Soft Targeting target, so if the player is facing an object (e.g. mailbox, chair), the character will perform Pick Lock on that object -- Game Issue #2: You'll get a "Invalid Target" error if the targeted lockbox is in the bank. (/use a bank item moves it into your bag) local _, addon = ... local _, _, classID = UnitClass("player"); local _, _, raceID = UnitRace("player"); if classID ~= 4 and raceID ~= 37 then return end; local API = addon.API; local IsWarningColor = API.IsWarningColor; local InCombatLockdown = InCombatLockdown; local IsSpellKnown = IsSpellKnown; local SpellIsTargeting = SpellIsTargeting; local GetMouseFocus = API.GetMouseFocus; local GetSpellInfo = API.GetSpellInfo; local GetCursorInfo = GetCursorInfo; local match = string.match; local IsInteractingWithNpcOfType = (C_PlayerInteractionManager and C_PlayerInteractionManager.IsInteractingWithNpcOfType) or function(type) return false end local TEXT_LOCKED = LOCKED or "Locked"; local SPELL_ID_PICK_LOCK = ((classID == 4) and 1804) or (312890); --Rogue: Lock Pick Mechagnome: Skeleton Pinkie local SPELL_NAME_PICK_LOCK = nil; --Localized with GetSpellInfo local INSTRUCTION_PICK_LOCK = addon.L["Instruction Pick Lock"]; local NOT_TRADED_ITEM_SLOT_INDEX = 7; --TRADE_ENCHANT_SLOT local MODULE_ENABLED = false; local TOOLTIP_CALLBACK_ADDED = false; local OLD_BAG_ID, OLD_SLOT_ID = nil, nil; local Processor = CreateFrame("Frame"); local TooltipFrame; local ActiveActionButton; local CursorProgressIndicator; local function HideActionButton() if ActiveActionButton then ActiveActionButton:Release(); end end local function HideProgressIndicator() if CursorProgressIndicator then CursorProgressIndicator:ClearWatch(); end end function Processor:OnUpdate_ProcessAfter(elapsed) self.t = self.t + elapsed; if self.t >= 0.0 then self:SetScript("OnUpdate", nil); self:ProcessItem(); end end function Processor:OnUpdate_CheckOwnerVisibility(elapsed) self.t = self.t + elapsed; if self.t >= 0.05 then self.t = 0; if (not self.owner) or (not self.owner:IsVisible()) then self:SetScript("OnUpdate", nil); HideActionButton(); HideProgressIndicator(); end end end function Processor:OnEvent(event, ...) if event == "CURSOR_CHANGED" then --Unused Event HideActionButton(); self:UnregisterEvent(event); elseif event == "UNIT_SPELLCAST_SUCCEEDED" then local _, _, spellID = ... if spellID == SPELL_ID_PICK_LOCK then self:UnregisterEvent(event); local bag, slot = OLD_BAG_ID, OLD_SLOT_ID; HideActionButton(); OLD_BAG_ID, OLD_SLOT_ID = bag, slot; --Item doesn't change status imediately so we pause our processing C_Timer.After(1, function() OLD_BAG_ID, OLD_SLOT_ID = nil, nil; end); end elseif event == "PLAYER_REGEN_DISABLED" then self:UnregisterEvent(event); self:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED"); self:SetScript("OnUpdate", nil); OLD_BAG_ID, OLD_SLOT_ID = nil, nil; elseif event == "TRADE_TARGET_ITEM_CHANGED" then local tradeSlotIndex = ... if tradeSlotIndex == NOT_TRADED_ITEM_SLOT_INDEX then HideActionButton(); end end end Processor:SetScript("OnEvent", Processor.OnEvent); function Processor:Initiate(customDelay) if customDelay then self.t = -customDelay; else self.t = 0; end self:SetScript("OnUpdate", self.OnUpdate_ProcessAfter); end local function IsPlayerInteracingBank() --Merchant is checked by another function --Banker, GuildBanker, MailInfo return IsInteractingWithNpcOfType(8) or IsInteractingWithNpcOfType(10) or IsInteractingWithNpcOfType(17) end local function ShouldShowOverlay() return (not InCombatLockdown()) and (not GetCursorInfo()) and (not SpellIsTargeting()) and IsSpellKnown(SPELL_ID_PICK_LOCK) and (not(MerchantFrame and MerchantFrame:IsShown())) end local function GetUnlockSpellName() if not SPELL_NAME_PICK_LOCK then SPELL_NAME_PICK_LOCK = GetSpellInfo(SPELL_ID_PICK_LOCK); end return SPELL_NAME_PICK_LOCK or "" end -- Copied from SharedTooltipTemplates.lua to prevent potential issues local function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset) local r, g, b = color:GetRGB(); if wrap == nil then wrap = true; end tooltip:AddLine(text, r, g, b, wrap, leftOffset); end local function GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrap, leftOffset) local leftR, leftG, leftB = leftColor:GetRGB(); local rightR, rightG, rightB = rightColor:GetRGB(); if wrap == nil then wrap = true; end tooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB, wrap, leftOffset); end local function AddLineDataText(tooltip, lineData, matchLineFunc) local leftText = lineData.leftText; local leftColor = lineData.leftColor or NORMAL_FONT_COLOR; local wrapText = lineData.wrapText or false; local rightText = lineData.rightText; local leftOffset = lineData.leftOffset; if rightText then local rightColor = lineData.rightColor or NORMAL_FONT_COLOR; GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrapText, leftOffset); elseif leftText then GameTooltip_AddColoredLine(tooltip, leftText, leftColor, wrapText, leftOffset); if matchLineFunc then return matchLineFunc(leftText); end end end -- End of the copy local function ActionButton_Bag_OnEnter(self) if self.bag and self.slot then TooltipFrame:Hide(); --[[ --This noticeably affects RAM count, so we use another way to display info local tooltipInfo = { getterName = "GetBagItem", getterArgs = {self.bag, self.slot, calledByPlumber = true}; }; TooltipFrame:ProcessInfo(tooltipInfo); --]] local tooltipData = C_TooltipInfo.GetBagItem(self.bag, self.slot); if not tooltipData then return end; TooltipFrame:SetOwner(self, "ANCHOR_LEFT"); for i, lineData in ipairs(tooltipData.lines) do AddLineDataText(TooltipFrame, lineData); end TooltipFrame:AddLine(INSTRUCTION_PICK_LOCK, 0.400, 0.733, 1.000, true); --Use a different color to distinguish it from other text in Pure Green TooltipFrame:Show(); end end local PATTERN_ITEM_PROPOSED_ENCHANT; do PATTERN_ITEM_PROPOSED_ENCHANT = string.gsub((ITEM_PROPOSED_ENCHANT or "Will receive %s."), "%.", "%%."); PATTERN_ITEM_PROPOSED_ENCHANT = string.gsub(PATTERN_ITEM_PROPOSED_ENCHANT, "%%s", "(%.+)"); --print(PATTERN_ITEM_PROPOSED_ENCHANT) end local function MatchProposedEnchantText(text) local enchantName = match(text, PATTERN_ITEM_PROPOSED_ENCHANT); if enchantName then return enchantName end end local function ActionButton_Trade_OnEnter(self) TooltipFrame:Hide(); local tooltipData = C_TooltipInfo.GetTradeTargetItem(NOT_TRADED_ITEM_SLOT_INDEX); if not tooltipData then return end; TooltipFrame:SetOwner(self, "ANCHOR_RIGHT"); local matchedText; local enchantTextFound = false; for i, lineData in ipairs(tooltipData.lines) do matchedText = AddLineDataText(TooltipFrame, lineData, MatchProposedEnchantText); if not enchantTextFound then enchantTextFound = matchedText ~= nil; end end if not enchantTextFound then TooltipFrame:AddLine(INSTRUCTION_PICK_LOCK, 0.400, 0.733, 1.000, true); end TooltipFrame:Show(); end local function ActionButton_OnHideCallback(self) Processor:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED"); Processor:UnregisterEvent("PLAYER_REGEN_DISABLED"); Processor:UnregisterEvent("TRADE_TARGET_ITEM_CHANGED"); Processor:SetScript("OnUpdate", nil); end local function ActionButton_OnLeave(self) OLD_BAG_ID = nil; OLD_SLOT_ID = nil; TooltipFrame:Hide(); self:Release(); HideProgressIndicator(); end local function SetupActionButton(bag, slot, tradeItem) if not ShouldShowOverlay() then return end; if not tradeItem then --When mouseover a lockbox in your bag while trading with another player --We don't create our ActionButton. By default, right-click put items in trade if IsInteractingWithNpcOfType(1) then return end end local privateKey = "RogueLockpick"; local ActionButton = addon.AcquireSecureActionButton(privateKey); ActionButton:SetSize(37, 37); ActionButton:SetPassThroughButtons("LeftButton"); ActionButton:SetFrameStrata("FULLSCREEN_DIALOG"); ActionButton:SetFixedFrameStrata(true); ActionButton:RegisterForClicks("RightButtonDown", "RightButtonUp"); ActionButton.bag = bag; ActionButton.slot = slot; ActionButton:SetScript("OnLeave", ActionButton_OnLeave); ActionButton.onHideCallback = ActionButton_OnHideCallback; ActionButton:ShowDebugHitRect(false); local spellName = GetUnlockSpellName(); local macroText; if tradeItem then macroText = string.format("/cast %s\r/click TradeRecipientItem%sItemButton", spellName, NOT_TRADED_ITEM_SLOT_INDEX); ActionButton:SetScript("OnEnter", ActionButton_Trade_OnEnter); else macroText = string.format("/cast %s\r/use %s %s", spellName, bag, slot); ActionButton:SetScript("OnEnter", ActionButton_Bag_OnEnter); end ActionButton:SetAttribute("type2", "macro"); ActionButton:SetMacroText(macroText); if not ActionButton.Highlight then local hl = ActionButton:CreateTexture(nil, "HIGHLIGHT"); ActionButton.Highlight = hl; hl:SetAllPoints(true); hl:SetTexture("Interface/AddOns/Plumber/Art/Button/Highlight-Square-Inner"); hl:SetBlendMode("ADD"); end return ActionButton end local function IsMouseoverItemLocked() local line2 = TooltipFrame.TextLeft2; if line2 and line2:GetText() == TEXT_LOCKED then local r, g, b = line2:GetTextColor(); if not IsWarningColor(r, g, b) then return true end end end local function IsMouseOverObjectItemButton(object) return object.GetBagID ~= nil end local function SetupButtonAndTooltip(bag, slot, tradeItem) local mouseoverObject = GetMouseFocus(); if mouseoverObject and IsMouseOverObjectItemButton(mouseoverObject) then local button = SetupActionButton(bag, slot, tradeItem); ActiveActionButton = button; if not button then return end; local x, y = mouseoverObject:GetCenter(); local scale = mouseoverObject:GetEffectiveScale(); local w, h = mouseoverObject:GetSize(); button:ClearAllPoints(); button:SetSize(w, h); button:SetScale(scale); button:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y); button:SetIgnoreParentScale(true); button:SetParent(UIParent); button:Show(); Processor:StartWatchingOwner(mouseoverObject); if not CursorProgressIndicator then CursorProgressIndicator = addon.AcquireCursorProgressIndicator(); end CursorProgressIndicator:ClearAllPoints(); CursorProgressIndicator:SetIgnoreParentScale(true); CursorProgressIndicator:SetScale(scale); CursorProgressIndicator:SetParent(UIParent); CursorProgressIndicator:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y); CursorProgressIndicator:WatchSpell(SPELL_ID_PICK_LOCK); end end function Processor:StartWatchingOwner(itemButton) self.owner = itemButton; self.t = 0; self:SetScript("OnUpdate", self.OnUpdate_CheckOwnerVisibility); self:RegisterEvent("PLAYER_REGEN_DISABLED"); self:RegisterEvent("TRADE_TARGET_ITEM_CHANGED"); self:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player"); end function Processor:ProcessItem() if InCombatLockdown() or IsPlayerInteracingBank() then return end; local info = TooltipFrame.processingInfo; if info and TooltipFrame:IsVisible() then if info.getterArgs and not info.getterArgs.calledByPlumber then if info.getterName == "GetBagItem" then local bag, slot = info.getterArgs[1], info.getterArgs[2]; if bag and slot then if IsMouseoverItemLocked() then SetupButtonAndTooltip(bag, slot); return end end elseif info.getterName == "GetTradeTargetItem" then local tradeSlotIndex = info.getterArgs[1]; if tradeSlotIndex and tradeSlotIndex == NOT_TRADED_ITEM_SLOT_INDEX then if IsMouseoverItemLocked() then SetupButtonAndTooltip(nil, nil, true); return end end end end end end function Processor:StopAll() HideActionButton(); HideProgressIndicator(); ActionButton_OnHideCallback(); end local function TooltipCall_SetInventoryItem(tooltip, ...) if not MODULE_ENABLED then return end; local info = tooltip.processingInfo; if info then if info.getterName == "GetBagItem" then Processor:Initiate(); end end end local function TooltipCall_SetTradeTargetItem(tooltip, ...) if not MODULE_ENABLED then return end; end local function Tooltip_OnSetBagItem(tooltip, bag, slot) if not MODULE_ENABLED then return end; if bag == OLD_BAG_ID and slot == OLD_SLOT_ID then return end OLD_BAG_ID = bag; OLD_SLOT_ID = slot; local info = tooltip.processingInfo; if info then if info.getterName == "GetBagItem" then Processor:Initiate(); end end end local function Tooltip_OnSetTradeTargetItem(tooltip, tradeSlotIndex) if not MODULE_ENABLED then return end; if tradeSlotIndex == NOT_TRADED_ITEM_SLOT_INDEX then Processor:Initiate(); end end local function AddTooltipPostCall() if TOOLTIP_CALLBACK_ADDED then return end; TOOLTIP_CALLBACK_ADDED = true; local tooltip = GameTooltip; if tooltip.SetBagItem then hooksecurefunc(tooltip, "SetBagItem", Tooltip_OnSetBagItem); end if tooltip.SetTradeTargetItem then hooksecurefunc(tooltip, "SetTradeTargetItem", Tooltip_OnSetTradeTargetItem); end end local function EnableModule(state) if state then MODULE_ENABLED = true; TooltipFrame = GameTooltip; AddTooltipPostCall(); GetUnlockSpellName(); else MODULE_ENABLED = false; Processor:StopAll(); end end do local moduleData = { name = addon.L["ModuleName HandyLockpick"], dbKey = "HandyLockpick", description = addon.L["ModuleDescription HandyLockpick"], toggleFunc = EnableModule, categoryID = 1, uiOrder = 100, }; addon.ControlCenter:AddModule(moduleData); end