diff --git a/api.lua b/api.lua index 6b3ba6a..aed85f4 100644 --- a/api.lua +++ b/api.lua @@ -109,6 +109,14 @@ API.playerstate = type(_G.IsMounted) == "function" and type(_G.CancelShapeshiftForm) == "function" API.unitguid = type(_G.UnitGUID) == "function" +API.unittoken = type(_G.UnitTokenFromGUID) == "function" +API.tooltipunit = _G.GameTooltip + and type(_G.GameTooltip.GetUnitGUID) == "function" +-- OnTooltipSetUnit shipped in ClassicAPI v1.8.2, but its per-tooltip +-- handler cells became reload-safe in v1.12.2. Require the fixed generation +-- instead of falling back to Vanilla OnShow/mouseover polling. +API.tooltipsetunit = API.classicapi_version >= 11202 + and API.tooltipunit and API.unittoken API.unitrange = type(_G.UnitInRange) == "function" API.macrospell = type(_G.GetMacroSpell) == "function" @@ -660,6 +668,19 @@ API.UnitGUID = function(unit) end end +API.UnitTokenFromGUID = function(guid) + if API.unittoken and guid then + return _G.UnitTokenFromGUID(guid) + end +end + +API.GetTooltipUnitGUID = function(tooltip) + if API.tooltipunit and tooltip then + local _, guid = tooltip:GetUnitGUID() + return guid + end +end + -- Prefer ClassicAPI's reach-aware 40-yard UnitInRange. The wrapper handles -- the player's own frame explicitly and preserves a Vanilla interaction check -- only as a centralized compatibility fallback. diff --git a/mods/tooltip-details.lua b/mods/tooltip-details.lua index 84592d4..df87a8d 100644 --- a/mods/tooltip-details.lua +++ b/mods/tooltip-details.lua @@ -5,45 +5,12 @@ local rgbhex = ShaguTweaks.rgbhex local Abbreviate = ShaguTweaks.Abbreviate local API = ShaguTweaks.API -local current_unit = "none" -local current_guid = nil -local statusbar = nil -local tooltip_from_mouseover = false local tooltipSetStatusBarColor -local direct_units = { "mouseover", "target", "focus", "player", "pet" } - -local function UnitMatchesGUID(unit, guid) - if not guid or not UnitExists(unit) then return false end - - if API and API.UnitGUID then - return API.UnitGUID(unit) == guid - elseif type(UnitGUID) == "function" then - return UnitGUID(unit) == guid - end - - return false -end - -local function GetTooltipGUID() - if type(GameTooltip.GetUnitGUID) == "function" then - local _, guid = GameTooltip:GetUnitGUID() - return guid - end -end - -local function TooltipMatchesMouseover() - if not UnitExists("mouseover") then return false end - - local guid = GetTooltipGUID() - if guid then - return UnitMatchesGUID("mouseover", guid) - end - - -- Compatibility fallback for clients without the ClassicAPI tooltip GUID. - local tooltipName = GameTooltipTextLeft1:GetText() - return tooltipName and - (UnitName("mouseover") == tooltipName or UnitPVPName("mouseover") == tooltipName) +local function GetUnit() + local guid = API.GetTooltipUnitGUID(GameTooltip) + if not guid then return end + return API.UnitTokenFromGUID(guid) end local function ClearTooltipBarColor() @@ -66,105 +33,37 @@ local function SetTooltipBarColor(r, g, b, a) end end -local function ResetTooltipIdentity() - current_unit = "none" - current_guid = nil - tooltip_from_mouseover = false +local function UpdateHealthText() + local backdrop = GameTooltipStatusBar.backdrop + if not backdrop or not backdrop.health then return end + + local hp = GameTooltipStatusBar:GetValue() + local _, hpmax = GameTooltipStatusBar:GetMinMaxValues() + + local hasRealHealth = hpmax > 100 + or (hpmax > 0 and round(hpmax / 100 * hp) ~= hp) + + if hasRealHealth then + backdrop.health:SetText(string.format("%s / %s", + Abbreviate(hp, true), Abbreviate(hpmax, true))) + elseif hpmax > 0 then + backdrop.health:SetText(string.format("%s%%", ceil(hp / hpmax * 100))) + else + backdrop.health:SetText("") + end +end + +local function ResetTooltipState() ClearTooltipBarColor() - if statusbar then - statusbar.name = nil - statusbar.level = nil - statusbar.lastHP = nil - statusbar.lastHPMax = nil - statusbar.lastName = nil - statusbar.lastLevel = nil - end -end - -local function FindUnitByGUID(guid) - if not guid then return end - - -- The common case is mouseover/target, so check cheap tokens first. - for _, unit in pairs(direct_units) do - if UnitMatchesGUID(unit, guid) then - return unit - end + if GameTooltipStatusBar.bg then + GameTooltipStatusBar.bg:SetVertexColor(.1, .1, 0, .8) end - for i=1,4 do - if UnitMatchesGUID("party" .. i, guid) then return "party" .. i end - if UnitMatchesGUID("partypet" .. i, guid) then return "partypet" .. i end + if GameTooltipStatusBar.backdrop + and GameTooltipStatusBar.backdrop.health then + GameTooltipStatusBar.backdrop.health:SetText("") end - - for i=1,40 do - if UnitMatchesGUID("raid" .. i, guid) then return "raid" .. i end - if UnitMatchesGUID("raidpet" .. i, guid) then return "raidpet" .. i end - end -end - -local function FindUnitByName() - local tooltipName = GameTooltipTextLeft1:GetText() - if not tooltipName then return end - - for _, unit in pairs(direct_units) do - if UnitExists(unit) and - (UnitName(unit) == tooltipName or UnitPVPName(unit) == tooltipName) then - return unit - end - end - - for i=1,4 do - local party = "party" .. i - local pet = "partypet" .. i - if UnitExists(party) and - (UnitName(party) == tooltipName or UnitPVPName(party) == tooltipName) then - return party - end - if UnitExists(pet) and - (UnitName(pet) == tooltipName or UnitPVPName(pet) == tooltipName) then - return pet - end - end - - for i=1,40 do - local raid = "raid" .. i - local pet = "raidpet" .. i - if UnitExists(raid) and - (UnitName(raid) == tooltipName or UnitPVPName(raid) == tooltipName) then - return raid - end - if UnitExists(pet) and - (UnitName(pet) == tooltipName or UnitPVPName(pet) == tooltipName) then - return pet - end - end -end - -local function GetUnit() - -- ClassicAPI stores the actual unit GUID on GameTooltip. Prefer it over the - -- old name-comparison heuristic, which can misidentify units with matching - -- display names or PvP titles. - local guid = GetTooltipGUID() - if guid then - if current_guid == guid and current_unit ~= "none" and - UnitMatchesGUID(current_unit, guid) then - return current_unit - end - - local unit = FindUnitByGUID(guid) - if unit then - current_guid = guid - current_unit = unit - return current_unit - end - end - - -- Compatibility fallback for clients without the ClassicAPI tooltip method, - -- or for a unit that cannot currently be mapped back to a known unit token. - current_guid = nil - current_unit = FindUnitByName() or "none" - return current_unit end local updating = false @@ -173,23 +72,18 @@ local function UpdateTooltip() if updating then return end updating = true + -- ClassicAPI keeps the exact tooltip GUID and resolves it back to a live + -- unit token natively. No party/raid scans or display-name fallback. local unit = GetUnit() - if unit == "none" then + if not unit then updating = false return end -- A newly resolved unit chooses its own class/reaction color below. Clear the - -- previous enforcement first so an unknown color cannot leak from the last - -- tooltip while the same GameTooltip frame is being reused. + -- previous enforcement first so a stale color cannot leak between tooltips. ClearTooltipBarColor() - -- Remember whether this particular unit tooltip came from the world - -- mouseover. If the same GUID is also the selected target, GetUnit() can - -- legitimately resolve it as "target" after mouseover ends; this flag keeps - -- the tooltip lifetime tied to the cursor instead of the target token. - tooltip_from_mouseover = TooltipMatchesMouseover() - local pvpname = UnitPVPName(unit) local name = UnitName(unit) local target = UnitName(unit .. "target") @@ -200,24 +94,15 @@ local function UpdateTooltip() local reaction = UnitReaction(unit, "player") local pvptitle = name and gsub(pvpname or name, " " .. name, "", 1) or nil - -- Keep the status display bound to the unit actually represented by the - -- tooltip, not merely whichever unit happened to be mouseover last. - if statusbar then - statusbar.name = name - statusbar.level = UnitLevel(unit) - statusbar.lastHP = nil - statusbar.lastHPMax = nil - statusbar.lastName = nil - statusbar.lastLevel = nil - end - if name then if UnitIsPlayer(unit) and class then local color = RAID_CLASS_COLORS[class] if color then SetTooltipBarColor(color.r, color.g, color.b, 1) - GameTooltipStatusBar.bg:SetVertexColor(color.r * 0.15, color.g * 0.15, color.b * 0.15, 0.8) - GameTooltipTextLeft1:SetText(rgbhex(color.r, color.g, color.b, color.a) .. name) + GameTooltipStatusBar.bg:SetVertexColor( + color.r * 0.15, color.g * 0.15, color.b * 0.15, 0.8) + GameTooltipTextLeft1:SetText( + rgbhex(color.r, color.g, color.b, color.a) .. name) else GameTooltipTextLeft1:SetText("|cff999999" .. name) end @@ -225,7 +110,8 @@ local function UpdateTooltip() local color = UnitReactionColor[reaction] if color then SetTooltipBarColor(color.r, color.g, color.b, 1) - GameTooltipStatusBar.bg:SetVertexColor(color.r * 0.15, color.g * 0.15, color.b * 0.15, 0.8) + GameTooltipStatusBar.bg:SetVertexColor( + color.r * 0.15, color.g * 0.15, color.b * 0.15, 0.8) end end @@ -279,6 +165,7 @@ local function UpdateTooltip() end end + UpdateHealthText() GameTooltip:Show() updating = false end @@ -299,6 +186,10 @@ local backdrop = { } module.enable = function(self) + -- Tooltip Details intentionally requires the native ClassicAPI tooltip + -- identity + OnTooltipSetUnit path. Do not fall back to Vanilla name scans. + if not API.tooltipsetunit then return end + GameTooltipStatusBar:SetHeight(10) GameTooltipStatusBar:ClearAllPoints() GameTooltipStatusBar:SetPoint("BOTTOMLEFT", GameTooltip, "TOPLEFT", 4, 2) @@ -309,90 +200,51 @@ module.enable = function(self) GameTooltipStatusBar.bg:SetVertexColor(.1, .1, 0, .8) GameTooltipStatusBar.bg:SetAllPoints(true) - GameTooltipStatusBar.backdrop = CreateFrame("Frame", "GameTooltipStatusBarBackdrop", GameTooltipStatusBar) - GameTooltipStatusBar.backdrop:SetPoint("TOPLEFT", GameTooltipStatusBar, "TOPLEFT", -3, 3) - GameTooltipStatusBar.backdrop:SetPoint("BOTTOMRIGHT", GameTooltipStatusBar, "BOTTOMRIGHT", 3, -3) + GameTooltipStatusBar.backdrop = + CreateFrame("Frame", "GameTooltipStatusBarBackdrop", GameTooltipStatusBar) + GameTooltipStatusBar.backdrop:SetPoint( + "TOPLEFT", GameTooltipStatusBar, "TOPLEFT", -3, 3) + GameTooltipStatusBar.backdrop:SetPoint( + "BOTTOMRIGHT", GameTooltipStatusBar, "BOTTOMRIGHT", 3, -3) GameTooltipStatusBar.backdrop:SetBackdrop(backdrop) GameTooltipStatusBar.backdrop:SetBackdropBorderColor(.8,.8,.8,1) - GameTooltipStatusBar.backdrop.health = GameTooltipStatusBar.backdrop:CreateFontString("Status", "DIALOG", "GameFontWhite") - GameTooltipStatusBar.backdrop.health:SetFont(STANDARD_TEXT_FONT, 12, "OUTLINE") + GameTooltipStatusBar.backdrop.health = + GameTooltipStatusBar.backdrop:CreateFontString( + "Status", "DIALOG", "GameFontWhite") + GameTooltipStatusBar.backdrop.health:SetFont( + STANDARD_TEXT_FONT, 12, "OUTLINE") GameTooltipStatusBar.backdrop.health:SetPoint("TOP", 0, 4) GameTooltipStatusBar.backdrop.health:SetNonSpaceWrap(false) -- Preserve the real statusbar method for Blizzard and other addons. A -- post-hook reapplies ShaguTweaks' class/reaction color only while a unit -- tooltip has an active desired color. - tooltipSetStatusBarColor = tooltipSetStatusBarColor or GameTooltipStatusBar.SetStatusBarColor + tooltipSetStatusBarColor = + tooltipSetStatusBarColor or GameTooltipStatusBar.SetStatusBarColor if not self.statusColorHooked then self.statusColorHooked = true - ShaguTweaks.hooksecurefunc(GameTooltipStatusBar, "SetStatusBarColor", function() - local r = GameTooltipStatusBar.ShaguTweaksColorR - local g = GameTooltipStatusBar.ShaguTweaksColorG - local b = GameTooltipStatusBar.ShaguTweaksColorB - local a = GameTooltipStatusBar.ShaguTweaksColorA - if r and g and b then - tooltipSetStatusBarColor(GameTooltipStatusBar, r, g, b, a or 1) - end - end) + ShaguTweaks.hooksecurefunc( + GameTooltipStatusBar, "SetStatusBarColor", function() + local r = GameTooltipStatusBar.ShaguTweaksColorR + local g = GameTooltipStatusBar.ShaguTweaksColorG + local b = GameTooltipStatusBar.ShaguTweaksColorB + local a = GameTooltipStatusBar.ShaguTweaksColorA + if r and g and b then + tooltipSetStatusBarColor( + GameTooltipStatusBar, r, g, b, a or 1) + end + end) end - -- update tooltip whenever it gets shown - local details = CreateFrame("Frame", nil, GameTooltip) - details:SetScript("OnShow", UpdateTooltip) - details:SetScript("OnHide", ResetTooltipIdentity) + -- ClassicAPI fires this after the native unit-tooltip builder completes. + -- This replaces OnShow + UPDATE_MOUSEOVER_UNIT identity refreshes. + GameTooltip:HookScript("OnTooltipSetUnit", UpdateTooltip) + -- OnTooltipCleared fires for every new Set* path, not only when the frame + -- hides, so unit-only state cannot leak into a following item/spell tooltip. + GameTooltip:HookScript("OnTooltipCleared", ResetTooltipState) - -- refresh the currently displayed unit identity - statusbar = CreateFrame("Frame", nil, GameTooltipStatusBar) - statusbar:RegisterEvent("UPDATE_MOUSEOVER_UNIT") - statusbar:SetScript("OnEvent", function() - -- Do not force Hide/FadeOut here. Turtle/Vanilla owns the tooltip lifetime - -- and applies its native delay + fade when the cursor leaves a world unit. - -- We only avoid re-showing the stale tooltip through the selected target. - if not UnitExists("mouseover") and tooltip_from_mouseover then - return - end - - local unit = GetUnit() - if unit ~= "none" then - this.name = UnitName(unit) - this.level = UnitLevel(unit) - else - this.name = UnitName("mouseover") - this.level = UnitLevel("mouseover") - end - - this.lastHP = nil - this.lastHPMax = nil - this.lastName = nil - this.lastLevel = nil - UpdateTooltip() - end) - - -- Keep the lightweight OnUpdate because the vanilla tooltip statusbar can - -- change without a dedicated Lua event. Skip string work entirely while its - -- inputs are unchanged. - statusbar:SetScript("OnUpdate", function() - local hp = GameTooltipStatusBar:GetValue() - local _, hpmax = GameTooltipStatusBar:GetMinMaxValues() - - if hp == this.lastHP and hpmax == this.lastHPMax and - this.name == this.lastName and this.level == this.lastLevel then - return - end - - this.lastHP = hp - this.lastHPMax = hpmax - this.lastName = this.name - this.lastLevel = this.level - - local hasRealHealth = hpmax > 100 or (round(hpmax / 100 * hp) ~= hp) - if hasRealHealth then - GameTooltipStatusBar.backdrop.health:SetText(string.format("%s / %s", Abbreviate(hp, true), Abbreviate(hpmax, true))) - elseif hpmax > 0 then - GameTooltipStatusBar.backdrop.health:SetText(string.format("%s%%", ceil(hp / hpmax * 100))) - else - GameTooltipStatusBar.backdrop.health:SetText("") - end - end) + -- StatusBar already has a native value-change script, so health text can be + -- event-driven too. No permanent OnUpdate is needed. + GameTooltipStatusBar:HookScript("OnValueChanged", UpdateHealthText) end