You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

474 lines
15 KiB

-- 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 <Action> 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