Files
ShaguTweaks-ClassicAPI/mods/actionbar-range-color.lua
T
OEVGandClaude Opus 5.5 1ae8398a2d Bound the player cache and stop per-frame range checks
- libunitscan: expire the shared player cache once per session (180 days
  for known classes, 90 for class-less, at most 10,000 players). Every
  sighting now stamps an age, so roster and mouseover entries can expire.
- libunitscan: keep a player's GUID only while the class is unknown and
  resolve such entries later with GetPlayerInfoByGUID.
- libunitscan: skip the GUID lookup for known chat speakers seen within
  the hour, throttle full guild rescans to once a minute, skip UnitGUID
  when UnitClass answered, and query ClassicAPI's player cache once per
  miss instead of twice.
- Social Colors: drop its separate 180-day cleanup.
- Range Color: use its own 0.2 s timer. Sharing Blizzard's rangeTimer ran
  the check every frame for every ranged button.
- Docs/PERFORMANCE.md: document the cache rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 00:09:59 +00:00

99 lines
4.2 KiB
Lua

local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Range Color"],
description = T["Action buttons will be colored red when out of range."],
expansions = { ["vanilla"] = true },
category = T["Action Bar"],
enabled = nil,
})
-- Modern APIs return booleans; legacy range APIs return 1/0/nil.
-- In Lua, numeric zero is truthy, so normalize both contracts explicitly.
local function NormalizeRange(value)
if value == true or value == 1 then return 1 end
if value == false or value == 0 then return 0 end
end
local function GetActionRange(action)
if not UnitExists("target") then return nil end
if API and API.GetActionInfo then
local actionType, id = API.GetActionInfo(action)
if actionType == "spell" and id and API.spellrange
and API.IsSpellInRange then
-- ClassicAPI's check is geometric: it does not reject the wrong
-- faction. A friendly buff should not turn red against an enemy.
local spells = _G.C_Spell
if spells then
if type(spells.IsSpellHelpful) == "function"
and spells.IsSpellHelpful(id) and type(UnitCanAssist) == "function"
and not UnitCanAssist("player", "target") then return nil end
if type(spells.IsSpellHarmful) == "function"
and spells.IsSpellHarmful(id) and type(UnitCanAttack) == "function"
and not UnitCanAttack("player", "target") then return nil end
end
-- nil is authoritative: self/rangeless spells have no range to
-- fail. Never turn it into an old action-slot false result.
return NormalizeRange(API.IsSpellInRange(id, "target"))
elseif actionType == "item" and id and API.itemrange
and API.IsItemInRange then
return NormalizeRange(API.IsItemInRange(id, "target"))
end
end
-- Macros, unresolved actions and clients without the native capability
-- retain the action-slot compatibility fallback.
return NormalizeRange(IsActionInRange(action))
end
local RANGE_INTERVAL = TOOLTIP_UPDATE_TIME or 0.2
module.enable = function(self)
hooksecurefunc("ActionButton_OnUpdate", function(elapsed)
-- Blizzard sets rangeTimer only for actions that have a range. Use our
-- own countdown instead of sharing it: decrementing rangeTimer here and
-- testing "<= 0.2" ran this check on every frame for every ranged
-- button, and kept Blizzard's own range check from ever firing.
if this.rangeTimer then
local timer = (this.shaguRangeTimer or 0) - (elapsed or 0)
if timer > 0 then
this.shaguRangeTimer = timer
else
this.shaguRangeTimer = RANGE_INTERVAL
local action = ActionButton_GetPagedID(this)
local inRange = GetActionRange(action)
-- Look the icon up once instead of building its name each time.
local icon = this.shaguRangeIcon
if not icon then
icon = _G[this:GetName() .. "Icon"]
this.shaguRangeIcon = icon
end
if icon then
if inRange == 0 then
if not this.a then
this.r, this.g, this.b, this.a = 0.85, 0.18, 0.18, 1
end
icon:SetVertexColor(this.r, this.g, this.b, this.a)
else
-- Restore the normal usability tint even if the old
-- red action is now unusable or short of mana.
local usable, noMana = IsUsableAction(action)
if usable and usable ~= 0 then
icon:SetVertexColor(1, 1, 1, 1)
elseif noMana and noMana ~= 0 then
icon:SetVertexColor(0.5, 0.5, 1, 1)
else
icon:SetVertexColor(0.4, 0.4, 0.4, 1)
end
end
end
end
end
end, true)
end