├── .github └── workflows │ └── release.yml ├── .gitignore ├── .luacheckrc ├── BarIntegrations.lua ├── ClassicCompat.lua ├── ColorGradient.lua ├── Controller.lua ├── Controller.xml ├── LICENSE.txt ├── LiteButtonAuras.toc ├── Localization.lua ├── Options.lua ├── Overlay.lua ├── Overlay.xml ├── README.md ├── SlashCommand.lua ├── SpellData.lua ├── Textures ├── Overlay.tga └── Square_FullWhite.tga ├── UI ├── AceGUIWidgets-LBAAnchorButtons.lua ├── AceGUIWidgets-LBAInputFocus.lua ├── AceGUIWidgets-LBAInputSpellID.lua ├── AceGUIWidgets-LBAInputValidSpell.lua └── Options.lua ├── embeds.xml ├── fetchlocale.sh ├── get-libs.sh └── pkgmeta.yaml /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Package and release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | 10 | release: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | CF_API_KEY: ${{ secrets.CF_API_KEY }} 16 | WOWI_API_TOKEN: ${{ secrets.WOWI_API_TOKEN }} 17 | WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }} 18 | GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | steps: 21 | 22 | - name: Clone project 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Package and release 28 | uses: BigWigsMods/packager@v2 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .release 2 | Libs/* 3 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | exclude_files = { 2 | ".luacheckrc", 3 | "Tests/", 4 | "Libs/", 5 | } 6 | 7 | -- https://luacheck.readthedocs.io/en/stable/warnings.html 8 | 9 | ignore = { 10 | "11./BINDING_.*", -- Setting an undefined (Keybinding) global variable 11 | "211", -- Unused local variable 12 | "212", -- Unused argument 13 | "213", -- Unused loop variable 14 | "432/self", -- Shadowing a local variable 15 | "542", -- empty if branch 16 | "631", -- line too long 17 | } 18 | 19 | globals = { 20 | "LiteButtonAurasControllerMixin", 21 | "LiteButtonAurasOverlayMixin", 22 | "SlashCmdList", 23 | } 24 | 25 | read_globals = { 26 | "ABP_NS", 27 | "ADD", 28 | "ActionBarButtonEventsFrame", 29 | "ActionButton_HideOverlayGlow", 30 | "ActionButton_SetupOverlayGlow", 31 | "AuraUtil", 32 | "C_UnitAuras", 33 | "C_Item", 34 | "C_Spell", 35 | "C_SpellBook", 36 | "ChatFontNormal", 37 | "ContinuableContainer", 38 | "CopyTable", 39 | "CreateFrame", 40 | "CreateFromMixins", 41 | "DEFAULT", 42 | "DELETE", 43 | "DebuffTypeColor", 44 | "Dominos", 45 | "GAMEMENU_HELP", 46 | "GENERAL", 47 | "GRAY_FONT_COLOR", 48 | "GameFontHighlight", 49 | "GameFontNormal", 50 | "GameTooltip", 51 | "GameTooltip_Hide", 52 | "GetActionInfo", 53 | "GetActionText", 54 | "GetKeysArray", 55 | "GetLocale", 56 | "GetMacroIndexByName", 57 | "GetMacroItem", 58 | "GetMacroSpell", 59 | "GetTime", 60 | "GetTotemInfo", 61 | "GetValuesArray", 62 | "GetWeaponEnchantInfo", 63 | "HIGHLIGHT_FONT_COLOR", 64 | "HasAction", 65 | "IsMouseButtonDown", 66 | "IsPlayerSpell", 67 | "IsSpellOverlayed", 68 | "LibStub", 69 | "LiteButtonAurasController", 70 | "MAX_TOTEMS", 71 | "Mixin", 72 | "NONE", 73 | "NORMAL_FONT_COLOR", 74 | "NumberFontNormal", 75 | "OKAY", 76 | "ORANGE_FONT_COLOR", 77 | "REMOVE", 78 | "SELECTED_CHAT_FRAME", 79 | "SETTINGS", 80 | "Settings", 81 | "Spell", 82 | "UIParent", 83 | "UnitCanAttack", 84 | "UnitCastingInfo", 85 | "UnitChannelInfo", 86 | "UnitIsFriend", 87 | "WOW_PROJECT_CLASSIC", 88 | "WOW_PROJECT_ID", 89 | "WithinRange", 90 | "format", 91 | "hooksecurefunc", 92 | "sort", 93 | "strsplit", 94 | "tContains", 95 | "tDeleteItem", 96 | "table", 97 | } 98 | -------------------------------------------------------------------------------- /BarIntegrations.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | LiteButtonAuras 4 | Copyright 2021 Mike "Xodiv" Battersby 5 | 6 | Create overlays for actionbuttons and hook update when they change. Note 7 | that hooksecurefunc() is kinda slow and should be avoided in cases where 8 | the actionbutton provides its own hook. 9 | 10 | ----------------------------------------------------------------------------]]-- 11 | 12 | local _, LBA = ... 13 | 14 | LBA.BarIntegrations = {} 15 | 16 | local C_Item = LBA.C_Item or C_Item 17 | local C_Spell = LBA.C_Spell or C_Spell 18 | 19 | local GetActionInfo = GetActionInfo 20 | local HasAction = HasAction 21 | 22 | -- Generic --------------------------------------------------------------------- 23 | 24 | local function GenericGetActionID(overlay) 25 | return overlay:GetParent().action 26 | end 27 | 28 | local function GenericGetActionInfo(overlay) 29 | return GetActionInfo(overlay:GetParent().action) 30 | end 31 | 32 | local function GenericHasAction(overlay) 33 | return HasAction(overlay:GetParent().action) 34 | end 35 | 36 | local function GenericInitButton(actionButton) 37 | local overlay = LiteButtonAurasController:CreateOverlay(actionButton) 38 | overlay.GetActionID = GenericGetActionID 39 | overlay.GetActionInfo = GenericGetActionInfo 40 | overlay.HasAction = GenericHasAction 41 | 42 | if not overlay.isHooked then 43 | hooksecurefunc(actionButton, 'Update', function () overlay:Update() end) 44 | overlay.isHooked = true 45 | end 46 | end 47 | 48 | -- Blizzard Classic ------------------------------------------------------------ 49 | 50 | -- Classic doesn't have an 'Update' method on the ActionButtons to hook 51 | -- so we have to hook the global function ActionButton_Update 52 | 53 | local function ClassicButtonUpdate(actionButton) 54 | local overlay = LiteButtonAurasController:GetOverlay(actionButton) 55 | if overlay then overlay:Update() end 56 | end 57 | 58 | local function ClassicInitButton(actionButton) 59 | local overlay = LiteButtonAurasController:CreateOverlay(actionButton) 60 | overlay.GetActionID = GenericGetActionID 61 | overlay.GetActionInfo = GenericGetActionInfo 62 | overlay.HasAction = GenericHasAction 63 | end 64 | 65 | function LBA.BarIntegrations:ClassicInit() 66 | if WOW_PROJECT_ID == 1 then return end 67 | for _, actionButton in pairs(ActionBarButtonEventsFrame.frames) do 68 | if actionButton:GetName():sub(1,8) ~= 'Override' then 69 | ClassicInitButton(actionButton) 70 | end 71 | end 72 | hooksecurefunc('ActionButton_Update', ClassicButtonUpdate) 73 | end 74 | 75 | -- Blizzard Retail ------------------------------------------------------------- 76 | 77 | -- The OverrideActionButtons have the same action (ID) as the main buttons and 78 | -- we don't want to handle them. 79 | 80 | function LBA.BarIntegrations:RetailInit() 81 | if WOW_PROJECT_ID ~= 1 then return end 82 | for _, actionButton in pairs(ActionBarButtonEventsFrame.frames) do 83 | if actionButton:GetName():sub(1,8) ~= 'Override' then 84 | GenericInitButton(actionButton) 85 | end 86 | end 87 | end 88 | 89 | 90 | -- Button Forge ---------------------------------------------------------------- 91 | 92 | -- These are ActionButton but they don't use the action ID they are set up as per 93 | -- SecureActionButtonTemplate with SetAttribute("type", ...) etc. 94 | -- 95 | -- The hook here on widget.icon.SetTexture is not exactly kosher but it does work. 96 | -- Hoping the author will add a BUTTON_UPDATE calback hook or similar. 97 | 98 | -- Localize for Minor speedup 99 | local ButtonForge_API1 100 | 101 | local function ButtonForgeGetActionID(overlay) 102 | return 0 103 | end 104 | 105 | -- Note that this returns the old-style Blizzard GetActionInfo where macro 106 | -- never returns a subType and id is always the macro ID. So it doesn't have the 107 | -- bugs that the new style does with item macros. 108 | -- See LiteButtonAurasOverlayMixin:SetUpAction() where type == "macro" 109 | 110 | local function ButtonForgeGetActionInfo(overlay) 111 | local widget = overlay:GetParent() 112 | return ButtonForge_API1.GetButtonActionInfo(widget:GetName()) 113 | end 114 | 115 | -- The buttons are re-used, but it's ok because CreateOverlay checks for that 116 | 117 | local function ButtonForgeInitButton(widget) 118 | local overlay = LiteButtonAurasController:CreateOverlay(widget) 119 | overlay.GetActionID = ButtonForgeGetActionID 120 | overlay.GetActionInfo = ButtonForgeGetActionInfo 121 | overlay.HasAction = ButtonForgeGetActionInfo 122 | hooksecurefunc(widget.icon, 'SetTexture', function () overlay:Update() end) 123 | end 124 | 125 | local function ButtonForgeCallback(_, event, actionButtonName) 126 | if event == "BUTTON_ALLOCATED" then 127 | local widget = _G[actionButtonName] 128 | ButtonForgeInitButton(widget) 129 | --[[ 130 | -- This would be nicer than hooking .icon.SetTexture if it got implemented. 131 | elseif event == "BUTTON_UPDATED" then 132 | local widget = _G[actionButtonName] 133 | local overlay = LiteButtonAurasController:GetOverlay(widget) 134 | if overlay then overlay:Update() end 135 | ]] 136 | end 137 | end 138 | 139 | function LBA.BarIntegrations:ButtonForgeInit() 140 | ButtonForge_API1 = _G.ButtonForge_API1 141 | if ButtonForge_API1 then 142 | ButtonForge_API1.RegisterCallback(ButtonForgeCallback) 143 | end 144 | end 145 | 146 | 147 | -- Dominos --------------------------------------------------------------------- 148 | 149 | -- On classic Dominos re-uses the Blizzard action buttons and then adds some 150 | -- more of its own. On retail it uses all its own buttons, but they still use 151 | -- the ActionBarButton API enough for us. 152 | 153 | function LBA.BarIntegrations:DominosInit() 154 | local Init = WOW_PROJECT_ID == 1 and GenericInitButton or ClassicInitButton 155 | if Dominos and not Dominos.BlizzardActionButtons then 156 | -- "New" dominos with their own buttons 157 | for actionButton in pairs(Dominos.ActionButtons.buttons) do 158 | Init(actionButton) 159 | end 160 | hooksecurefunc(Dominos.ActionButton, 'OnCreate', 161 | function (button, id) Init(button) end) 162 | end 163 | end 164 | 165 | 166 | -- ActionBarPlus --------------------------------------------------------------- 167 | 168 | -- All SecureActionButton without any actionID 169 | 170 | -- I'm not 100% convinced about the wisdom of supporting this. The addon is 171 | -- overengineered and still doesn't support basic things like putting a pet 172 | -- action on a button. The code is inscrutable to me and looks like the kind 173 | -- of thing you get when you believe boolean should be a class and have a 174 | -- BooleanFactory to create one. But this does seem to work. 175 | 176 | local function ABPGetActionID(overlay) 177 | return 0 178 | end 179 | 180 | local function ABPGetActionInfo(overlay) 181 | local button = overlay:GetParent() 182 | local type = button:GetAttribute("type") 183 | if type == 'spell' then 184 | local spell = button:GetAttribute('spell') 185 | local info = C_Spell.GetSpellInfo(spell) 186 | if info then return type, info.spellID end 187 | elseif type == 'macro' then 188 | local id = button:GetAttribute('macro') 189 | if id then return type, id end 190 | elseif type == 'item' then 191 | local item = button:GetAttribute('item') 192 | local id = C_Item.GetItemInfoInstant(item) 193 | if id then return type, id end 194 | end 195 | end 196 | 197 | local function ABPHasAction(overlay) 198 | local button = overlay:GetParent() 199 | return not button.widget:IsEmpty() 200 | end 201 | 202 | local function ABPInitButton(actionButton) 203 | local overlay = LiteButtonAurasController:CreateOverlay(actionButton) 204 | overlay:SetFrameLevel(actionButton.widget.cooldown():GetFrameLevel() + 1) 205 | 206 | overlay.GetActionID = ABPGetActionID 207 | overlay.GetActionInfo = ABPGetActionInfo 208 | overlay.HasAction = ABPHasAction 209 | 210 | if not overlay.isHooked then 211 | actionButton:HookScript('OnAttributeChanged', function () overlay:Update() end) 212 | hooksecurefunc(actionButton.widget, 'UpdateMacroState', function () overlay:Update() end) 213 | overlay.isHooked = true 214 | end 215 | end 216 | 217 | local function ABPInitFrameWidget(actionBar) 218 | for _, actionButton in ipairs(actionBar.buttonFrames) do 219 | ABPInitButton(actionButton) 220 | end 221 | end 222 | 223 | function LBA.BarIntegrations:ActionbarPlusInit() 224 | if ABP_NS then 225 | for _, actionBar in ipairs(ABP_NS.O.ButtonFactory.FRAMES) do 226 | ABPInitFrameWidget(actionBar) 227 | end 228 | hooksecurefunc(ABP_NS.O.ButtonFactory, 'CreateButtons', 229 | function (self, fw, rowSize, colSize) 230 | ABPInitFrameWidget(fw) 231 | end) 232 | end 233 | end 234 | 235 | 236 | -- LibActionButton-1.0 and derivatives ----------------------------------------- 237 | 238 | -- Covers ElvUI, Bartender. TukUI reuses the Blizzard buttons 239 | 240 | local function LABGetActionID(overlay) 241 | local actionType, action = overlay:GetParent():GetAction() 242 | if actionType == "action" then 243 | return action 244 | end 245 | end 246 | 247 | local function LABGetActionInfo(overlay) 248 | local actionType, action = overlay:GetParent():GetAction() 249 | if actionType == "action" then 250 | return GetActionInfo(action) 251 | else 252 | return actionType, action 253 | end 254 | end 255 | 256 | local function LABHasAction(overlay) 257 | local actionType, action = overlay:GetParent():GetAction() 258 | if actionType == "action" then 259 | return HasAction(action) 260 | end 261 | end 262 | 263 | local function LABInitButton(event, actionButton) 264 | local overlay = LiteButtonAurasController:CreateOverlay(actionButton) 265 | overlay.GetActionID = LABGetActionID 266 | overlay.GetActionInfo = LABGetActionInfo 267 | overlay.HasAction = LABHasAction 268 | overlay:Update() 269 | end 270 | 271 | -- LAB doesn't fire OnButtonCreated until the end of CreateButton but 272 | -- fires OnButtonUpdate in the middle, so we get Update before Create, 273 | -- hence the "if". 274 | 275 | local function LABButtonUpdate(event, actionButton) 276 | local overlay = LiteButtonAurasController:GetOverlay(actionButton) 277 | if overlay then overlay:Update() end 278 | end 279 | 280 | -- As far as I can tell there aren't any buttons at load time but just 281 | -- in case. 282 | 283 | local function LABInitAllButtons(lib) 284 | for actionButton in pairs(lib:GetAllButtons()) do 285 | LABInitButton(nil, actionButton) 286 | end 287 | end 288 | 289 | -- The %- here is a literal - instead of "zero or more repetitions". A 290 | -- few addons (most noteably ElvUI) use their own private version of 291 | -- LibActionButton with a suffix added to the name. 292 | 293 | function LBA.BarIntegrations:LABInit() 294 | for name, lib in LibStub:IterateLibraries() do 295 | if name:match('^LibActionButton%-1.0') then 296 | LABInitAllButtons(lib) 297 | lib.RegisterCallback(self, 'OnButtonCreated', LABInitButton) 298 | lib.RegisterCallback(self, 'OnButtonUpdate', LABButtonUpdate) 299 | end 300 | end 301 | end 302 | 303 | -- Init ------------------------------------------------------------------------ 304 | 305 | function LBA.BarIntegrations:Initialize() 306 | self:RetailInit() 307 | self:ClassicInit() 308 | self:DominosInit() 309 | self:ButtonForgeInit() 310 | self:LABInit() 311 | self:ActionbarPlusInit() 312 | end 313 | -------------------------------------------------------------------------------- /ClassicCompat.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | LiteButtonAuras 4 | Copyright 2021 Mike "Xodiv" Battersby 5 | 6 | For better or worse, try to back-port a minimal amount of compatibility 7 | for the 11.0 rework into classic, on the assumption that it will eventually 8 | go in there properly and this is the right approach rather than making the 9 | new way look like the old. 10 | 11 | ----------------------------------------------------------------------------]]-- 12 | 13 | local _, LBA = ... 14 | 15 | 16 | -- C_Spell --------------------------------------------------------------------- 17 | 18 | LBA.C_Spell = CopyTable(C_Spell or {}) 19 | 20 | if not LBA.C_Spell.GetSpellInfo then 21 | local GetSpellInfo = _G.GetSpellInfo 22 | 23 | function LBA.C_Spell.GetSpellInfo(spellIdentifier) 24 | local name, _, iconID, castTime, minRange, maxRange, spellID, originalIconID = GetSpellInfo(spellIdentifier) 25 | if name then 26 | return { 27 | name = name, 28 | iconID = iconID, 29 | originalIconID = originalIconID, 30 | castTime = castTime, 31 | minRange = minRange, 32 | maxRange = maxRange, 33 | spellID = spellID, 34 | } 35 | end 36 | end 37 | end 38 | 39 | if not LBA.C_Spell.GetSpellName then 40 | local GetSpellInfo = _G.GetSpellInfo 41 | 42 | function LBA.C_Spell.GetSpellName(spellIdentifier) 43 | local name = GetSpellInfo(spellIdentifier) 44 | return name 45 | end 46 | end 47 | 48 | if not LBA.C_Spell.GetSpellTexture then 49 | local GetSpellInfo = _G.GetSpellInfo 50 | 51 | function LBA.C_Spell.GetSpellTexture(spellIdentifier) 52 | local _, _, iconID = GetSpellInfo(spellIdentifier) 53 | return iconID 54 | end 55 | end 56 | 57 | if not LBA.C_Spell.GetSpellCooldown then 58 | local GetSpellCooldown = _G.GetSpellCooldown 59 | 60 | function LBA.C_Spell.GetSpellCooldown(spellIdentifier) 61 | local startTime, duration, isEnabled, modRate = GetSpellCooldown(spellIdentifier) 62 | if startTime then 63 | return { 64 | startTime = startTime, 65 | duration = duration, 66 | isEnabled = isEnabled, 67 | modRate = modRate, 68 | } 69 | end 70 | end 71 | end 72 | 73 | 74 | -- C_Item ---------------------------------------------------------------------- 75 | 76 | LBA.C_Item = CopyTable(C_Item or {}) 77 | 78 | if not LBA.C_Item.GetItemInfoInstant then 79 | LBA.C_Item.GetItemInfoInstant = _G.GetItemInfoInstant 80 | end 81 | 82 | if not LBA.C_Item.GetItemSpell then 83 | LBA.C_Item.GetItemSpell = _G.GetItemSpell 84 | end 85 | 86 | 87 | -- AuraUtil -------------------------------------------------------------------- 88 | 89 | -- Classic doesn't have ForEachAura even though it has AuraUtil. 90 | 91 | LBA.AuraUtil = CopyTable(AuraUtil or {}) 92 | 93 | if not AuraUtil.ForEachAura then 94 | 95 | local UnitAura = _G.UnitAura 96 | 97 | -- Turn the UnitAura returns into a facsimile of the UnitAuraInfo struct 98 | -- returned by C_UnitAuras.GetAuraDataBySlot(unit, slot) 99 | 100 | local auraInstanceID = 0 101 | 102 | local function UnitAuraData(unit, i, filter) 103 | local name, icon, count, dispelType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod = UnitAura(unit, i, filter) 104 | 105 | local isHarmful = filter:find('HARMFUL') and true or false 106 | local isHelpful = filter:find('HELPFUL') and true or false 107 | 108 | auraInstanceID = auraInstanceID + 1 109 | return { 110 | applications = count, 111 | auraInstanceID = auraInstanceID, 112 | canApplyAura = canApplyAura, 113 | -- charges = , 114 | dispelName = dispelType, 115 | duration = duration, 116 | expirationTime = expirationTime, 117 | icon = icon, 118 | isBossAura = isBossDebuff, 119 | isFromPlayerOrPlayerPet = castByPlayer, -- player = me vs player = a player? 120 | isHarmful = isHarmful, 121 | isHelpful = isHelpful, 122 | -- isNameplateOnly = 123 | -- isRaid = 124 | isStealable = isStealable, 125 | -- maxCharges = 126 | name = name, 127 | nameplateShowAll = nameplateShowAll, 128 | nameplateShowPersonal = nameplateShowPersonal, 129 | -- points = 130 | sourceUnit = source, 131 | spellId = spellId, 132 | timeMod = timeMod, 133 | } 134 | end 135 | 136 | function LBA.AuraUtil.ForEachAura(unit, filter, maxCount, func, usePackedAura) 137 | local i = 1 138 | while true do 139 | if maxCount and i > maxCount then 140 | return 141 | elseif UnitAura(unit, i, filter) then 142 | if usePackedAura then 143 | func(UnitAuraData(unit, i, filter)) 144 | else 145 | func(UnitAura(unit, i, filter)) 146 | end 147 | else 148 | return 149 | end 150 | i = i + 1 151 | end 152 | end 153 | 154 | end 155 | -------------------------------------------------------------------------------- /ColorGradient.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | LiteButtonAuras 4 | Copyright 2021 Mike "Xodiv" Battersby 5 | 6 | I took the idea for the HLS gradients from AdiButtonAuras. The code 7 | is adapated from the python colorsys module but you can find the same 8 | algorithm on StackOverflow. 9 | 10 | Benchmarking this looks like LBA.TimerRGB takes about 0.5ns to run 11 | when the timer is >10s, and 2ns to run when it has to interpolate. 12 | At 100fps this is 200ns per second which seems fine to me. 13 | 14 | That's just the color calcuating though, whether or not its a good idea 15 | to run run SetFormattedText and SetTextColor every frame is different 16 | matter. BuffFrame does it though, so I do too! 17 | 18 | ----------------------------------------------------------------------------]]-- 19 | 20 | local _, LBA = ... 21 | 22 | LBA = LBA or {} 23 | 24 | local min, max = math.min, math.max 25 | 26 | local function hueToV(m1, m2, hue) 27 | hue = hue % 1 28 | if hue < 1/6 then 29 | return m1 + (m2-m1)*hue*6 30 | elseif hue < 1/2 then 31 | return m2 32 | elseif hue < 2/3 then 33 | return m1 + (m2-m1)*(2/3-hue)*6 34 | else 35 | return m1 36 | end 37 | end 38 | 39 | local function hlsToRgb(h, l, s) 40 | if s == 0 then 41 | return l, l, l 42 | end 43 | local m2 44 | if l < 0.5 then 45 | m2 = l * (1+s) 46 | else 47 | m2 = (l+s) - (l*s) 48 | end 49 | local m1 = 2*l - m2 50 | return hueToV(m1, m2, h+1/3), hueToV(m1, m2, h), hueToV(m1, m2, h-1/3) 51 | end 52 | 53 | local function rgbToHls(r, g, b) 54 | local minC, maxC = min(r, g, b), max(r, g, b) 55 | local l = (minC + maxC)/2 56 | if minC == maxC then 57 | return 0, l, 0 58 | end 59 | local h, s 60 | if l < 0.5 then 61 | s = (maxC-minC) / (maxC+minC) 62 | else 63 | s = (maxC-minC) / (2-maxC-minC) 64 | end 65 | local rc = (maxC-r) / (maxC-minC) 66 | local gc = (maxC-g) / (maxC-minC) 67 | local bc = (maxC-b) / (maxC-minC) 68 | if r == maxC then 69 | h = bc - gc 70 | elseif g == maxC then 71 | h = 2 + rc - bc 72 | else 73 | h = 4 + gc - rc 74 | end 75 | return (h/6) % 1, l, s 76 | end 77 | 78 | local function interpolateHls(perc, h1, l1, s1, h2, l2, s2) 79 | -- L and S are linear interpolated 80 | local l = l1 + (l2-l1) * perc 81 | local s = s1 + (s2-s1) * perc 82 | 83 | -- Hue is a degree coordinate in radians on a circle that wraps. We want 84 | -- the smallest of the two angles between them. 85 | local dh = h2 - h1 86 | if dh < -0.5 then 87 | dh = dh + 1 88 | elseif dh > 0.5 then 89 | dh = dh - 1 90 | end 91 | 92 | local h = (h1 + dh*perc) % 1 93 | return h, l, s 94 | end 95 | 96 | -- Colors in HLS so we don't have to do the math to convert them every frame. 97 | -- These are brighter than the pure rgb because the 1,0,0 red is too hard to 98 | -- see. 99 | 100 | local Red = { 0, 0.75, 1 } 101 | local Yellow = { 1/6, 0.75, 1 } 102 | local White = { 0, 1, 0 } 103 | 104 | -- In theory this could be memoized for the values < 10s because they are 105 | -- truncated to 0.1 of a second before this is called. But I don't know 106 | -- enough about math.ceil to know if that's safe, and I'm guaranteed to 107 | -- forget that at some point and blow out memory infinitely. 108 | 109 | function LBA.TimerRGB(duration) 110 | if duration <= 3 then 111 | return hlsToRgb( 112 | interpolateHls( 113 | duration/3, 114 | Red[1], Red[2], Red[3], 115 | Yellow[1], Yellow[2], Yellow[3] 116 | ) 117 | ) 118 | elseif duration <= 10 then 119 | return hlsToRgb( 120 | interpolateHls( 121 | (duration-3)/7, 122 | Yellow[1], Yellow[2], Yellow[3], 123 | White[1], White[2], White[3] 124 | ) 125 | ) 126 | else 127 | return 1, 1, 1 128 | end 129 | end 130 | 131 | --@debug@ 132 | LBA.interpolateHls = interpolateHls 133 | LBA.rgbToHls = rgbToHls 134 | LBA.hlsToRgb = hlsToRgb 135 | --@end-debug@ 136 | -------------------------------------------------------------------------------- /Controller.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | LiteButtonAuras 4 | Copyright 2021 Mike "Xodiv" Battersby 5 | 6 | This is the event handler and state updater. Watches for the buffs and 7 | updates LBA.state, then calls overlay:Update() on all actionbutton overlays 8 | when required. 9 | 10 | ----------------------------------------------------------------------------]]-- 11 | 12 | local addonName, LBA = ... 13 | 14 | local C_Spell = LBA.C_Spell or C_Spell 15 | 16 | local L = LBA.L 17 | 18 | LBA.state = { 19 | player = { 20 | buffs = {}, 21 | debuffs = {}, 22 | totems = {}, 23 | weaponEnchants = {}, 24 | channel = nil, 25 | }, 26 | pet = { 27 | buffs = {}, 28 | debuffs = {}, 29 | }, 30 | target = { 31 | buffs = {}, 32 | debuffs = {}, 33 | interrupt = nil, 34 | }, 35 | } 36 | 37 | 38 | --[[------------------------------------------------------------------------]]-- 39 | 40 | -- Cache some things to be faster. This is annoying but it's really a lot 41 | -- faster. Only do this for things that are called in the event loop otherwise 42 | -- it's just a pain to maintain. 43 | 44 | local AuraUtil = LBA.AuraUtil or AuraUtil 45 | local GetTotemInfo = GetTotemInfo 46 | local MAX_TOTEMS = MAX_TOTEMS 47 | local UnitCanAttack = UnitCanAttack 48 | local UnitCastingInfo = UnitCastingInfo 49 | local UnitChannelInfo = UnitChannelInfo 50 | local WOW_PROJECT_ID = WOW_PROJECT_ID 51 | 52 | --[[------------------------------------------------------------------------]]-- 53 | 54 | -- Load and set up dependencies for Masque support. Because we make our own 55 | -- frame and don't touch the ActionButton itself (avoids a LOT of taint issues) 56 | -- we have to make our own masque group. It's a bit weird because it lets you 57 | -- style LBA differently from the ActionButton, but it's the simplest way. 58 | 59 | local Masque = LibStub('Masque', true) 60 | local MasqueGroup = Masque and Masque:Group(addonName) 61 | 62 | 63 | --[[------------------------------------------------------------------------]]-- 64 | 65 | LiteButtonAurasControllerMixin = {} 66 | 67 | function LiteButtonAurasControllerMixin:OnLoad() 68 | self.overlayFrames = {} 69 | self:RegisterEvent('PLAYER_LOGIN') 70 | end 71 | 72 | function LiteButtonAurasControllerMixin:Initialize() 73 | 74 | -- At init time C_Item.GetItemSpell might not work because they are not 75 | -- in the cache. I think the actionbar will keep them in the cache the rest 76 | -- of the time. Relies on ITEM_DATA_LOAD_RESULT. 77 | LBA.buttonItemIDs = {} 78 | 79 | LBA.InitializeOptions() 80 | LBA.InitializeGUIOptions() 81 | LBA.SetupSlashCommand() 82 | LBA.UpdateAuraMap() 83 | 84 | -- Now this is be delayed until PLAYER_LOGIN do we still need to list 85 | -- list all possible LibActionButton derivatives in the TOC dependencies? 86 | LBA.BarIntegrations:Initialize() 87 | 88 | self:RegisterEvent('UNIT_AURA') 89 | self:RegisterEvent('PLAYER_ENTERING_WORLD') 90 | self:RegisterEvent('PLAYER_TARGET_CHANGED') 91 | self:RegisterEvent('PLAYER_TOTEM_UPDATE') 92 | if WOW_PROJECT_ID == 1 then 93 | self:RegisterEvent('WEAPON_ENCHANT_CHANGED') 94 | self:RegisterEvent('WEAPON_SLOT_CHANGED') 95 | end 96 | 97 | -- All of these are for the interrupt and player channel detection 98 | self:RegisterEvent('UNIT_SPELLCAST_START') 99 | self:RegisterEvent('UNIT_SPELLCAST_STOP') 100 | self:RegisterEvent('UNIT_SPELLCAST_DELAYED') 101 | self:RegisterEvent('UNIT_SPELLCAST_FAILED') 102 | self:RegisterEvent('UNIT_SPELLCAST_INTERRUPTED') 103 | self:RegisterEvent('UNIT_SPELLCAST_CHANNEL_START') 104 | self:RegisterEvent('UNIT_SPELLCAST_CHANNEL_STOP') 105 | self:RegisterEvent('UNIT_SPELLCAST_CHANNEL_UPDATE') 106 | self:RegisterEvent('UNIT_SPELLCAST_INTERRUPTIBLE') 107 | self:RegisterEvent('UNIT_SPELLCAST_NOT_INTERRUPTIBLE') 108 | self:RegisterEvent('ITEM_DATA_LOAD_RESULT') 109 | 110 | LBA.db.RegisterCallback(self, 'OnModified', 'StyleAllOverlays') 111 | end 112 | 113 | function LiteButtonAurasControllerMixin:CreateOverlay(actionButton) 114 | if not self.overlayFrames[actionButton] then 115 | local name = actionButton:GetName() .. "LiteButtonAurasOverlay" 116 | local overlay = CreateFrame('Frame', name, actionButton, "LiteButtonAurasOverlayTemplate") 117 | self.overlayFrames[actionButton] = overlay 118 | if MasqueGroup then 119 | MasqueGroup:AddButton(overlay, { 120 | SpellHighlight = overlay.Glow, 121 | Normal = false, 122 | -- Duration = overlay.Timer, 123 | -- Count = overlay.Count, 124 | }) 125 | end 126 | end 127 | return self.overlayFrames[actionButton] 128 | end 129 | 130 | function LiteButtonAurasControllerMixin:GetOverlay(actionButton) 131 | return self.overlayFrames[actionButton] 132 | end 133 | 134 | function LiteButtonAurasControllerMixin:UpdateAllOverlays(stateOnly) 135 | for _, overlay in pairs(self.overlayFrames) do 136 | overlay:Update(stateOnly) 137 | end 138 | end 139 | 140 | function LiteButtonAurasControllerMixin:StyleAllOverlays() 141 | for _, overlay in pairs(self.overlayFrames) do 142 | overlay:Style() 143 | overlay:Update() 144 | end 145 | end 146 | 147 | function LiteButtonAurasControllerMixin:DumpAllOverlays() 148 | self:UpdateAllOverlays() 149 | local sortedOverlays = GetValuesArray(self.overlayFrames) 150 | table.sort(sortedOverlays, function (a, b) return a:GetActionID() < b:GetActionID() end) 151 | for _, overlay in pairs(sortedOverlays) do 152 | overlay:Dump() 153 | end 154 | end 155 | 156 | --[[------------------------------------------------------------------------]]-- 157 | 158 | -- State updating local functions 159 | 160 | -- This could be made (probably) more efficient by using the 10.0 event 161 | -- argument auraUpdateInfo at the price of losing classic compatibility. 162 | -- 163 | -- "Probably" because once you do that you have to do your own "filtering" 164 | -- duplicating the 'HELPFUL PLAYER' etc. and iterate over a bunch of auras 165 | -- that aren't relevant here. It depends on how efficient the filter in 166 | -- UnitAuraSlots is (and by extension AuraUtil.ForEachAura). Would also have 167 | -- to either index them by auraInstanceID + scan for name in overlay, or 168 | -- keep indexing them by name and scan for auraInstanceID when updating. 169 | 170 | -- There's no point guessing at what would be better performance, if you're 171 | -- going to try to improve then measure it. Potentials for performance 172 | -- improvement (but measure!): 173 | -- 174 | -- * limit the aura scans by using a dirty/sweep 175 | -- * use the UNIT_AURA push data (as above) 176 | -- 177 | -- Overall the 10.0 changes are not that helpful for matching by name. 178 | -- 179 | -- It's worth noting that the 10.0 BuffFrame still uses the same mechanism 180 | -- as used here, but both the CompactUnitFrame and the TargetFrame have 181 | -- switched to using the new ways. 182 | -- 183 | -- { 184 | -- applications = 0, 185 | -- auraInstanceID = 154047, 186 | -- canApplyAura = true, 187 | -- duration = 3600, 188 | -- expirationTime = 9109.109, 189 | -- icon = 136051, 190 | -- isBossAura = false, 191 | -- isFromPlayerOrPlayerPet = true, 192 | -- isHarmful = false, 193 | -- isHelpful = true, 194 | -- isNameplateOnly = false, 195 | -- isRaid = false, 196 | -- isStealable = false 197 | -- name = "Lightning Shield", 198 | -- nameplateShowAll = false, 199 | -- nameplateShowPersonal = false, 200 | -- points = { }, 201 | -- sourceUnit = "player", 202 | -- spellId = 192106, 203 | -- timeMod = 1, 204 | -- } 205 | -- 206 | -- https://warcraft.wiki.gg/wiki/API_C_UnitAuras.GetAuraDataBySlot 207 | 208 | -- Also add a duplicate with any override name. This is awkward but there's no 209 | -- inverse of C_Spell.GetOverrideSpell. 210 | -- 211 | -- C_Spell.GetOverrideSpell returns the same ID if passed in an ID that's not 212 | -- overridden (seems like no check is done if it's a valid spell or not). 213 | -- 214 | -- It will return 0 if given a string that doesn't match to an ID (and not nil 215 | -- like all the other C_Spell.GetX functions). 216 | -- 217 | -- We call it with auraData.name because some of our faked up auraData (like 218 | -- for weapon enchants) doesn't have a spellId in it. 219 | 220 | local function UpdateTableAura(t, auraData) 221 | t[auraData.name] = auraData 222 | if C_Spell.GetOverrideSpell then 223 | local overrideID = C_Spell.GetOverrideSpell(auraData.name) 224 | local overrideName = C_Spell.GetSpellName(overrideID) 225 | if overrideName and overrideName ~= auraData.name then 226 | -- Doesn't update the spell name in the auraData only the index name 227 | t[overrideName] = auraData 228 | end 229 | end 230 | end 231 | 232 | -- Fake AuraData for weapon enchants, see BuffFrame.lua for how WoW does it 233 | local function WeaponEnchantAuraData(duration, charges, id) 234 | local name = LBA.WeaponEnchantSpellID[id] 235 | if name then 236 | return { 237 | isTempEnchant = true, 238 | auraType = "TempEnchant", 239 | applications = charges, 240 | duration = 0, 241 | expirationTime = GetTime() + duration/1000, 242 | name = name, 243 | } 244 | end 245 | end 246 | 247 | local function UpdateWeaponEnchants() 248 | -- Classic doesn't have the events to do this efficiently 249 | if WOW_PROJECT_ID ~= 1 then return end 250 | 251 | LBA.state.player.weaponEnchants = {} 252 | 253 | local mhEnchant, mhDuration, mhCharges, mhID, 254 | ohEnchant, ohDuration, ohCharges, ohID = GetWeaponEnchantInfo() 255 | 256 | if mhEnchant then 257 | local auraData = WeaponEnchantAuraData(mhDuration, mhCharges, mhID) 258 | if auraData then 259 | UpdateTableAura(LBA.state.player.weaponEnchants, auraData) 260 | end 261 | end 262 | if ohEnchant then 263 | local auraData = WeaponEnchantAuraData(ohDuration, ohCharges, ohID) 264 | if auraData then 265 | UpdateTableAura(LBA.state.player.weaponEnchants, auraData) 266 | end 267 | end 268 | end 269 | 270 | local function UpdateUnitAuras(unit, auraInfo) 271 | 272 | -- XXX TODO handle auraInfo for efficiency 273 | 274 | LBA.state[unit].buffs = {} 275 | LBA.state[unit].debuffs = {} 276 | 277 | if UnitCanAttack('player', unit) then 278 | -- Hostile target buffs are only for dispels 279 | AuraUtil.ForEachAura(unit, 'HELPFUL', nil, 280 | function (auraData) 281 | UpdateTableAura(LBA.state[unit].buffs, auraData) 282 | end, 283 | true) 284 | AuraUtil.ForEachAura(unit, 'HARMFUL PLAYER', nil, 285 | function (auraData) 286 | UpdateTableAura(LBA.state[unit].debuffs, auraData) 287 | end, 288 | true) 289 | else 290 | AuraUtil.ForEachAura(unit, 'HELPFUL PLAYER', nil, 291 | function (auraData) 292 | UpdateTableAura(LBA.state[unit].buffs, auraData) 293 | end, 294 | true) 295 | -- Inclue long-lasting buffs we can cast even if applied 296 | -- by someone else, since we don't care who cast Battle Shout, etc. 297 | AuraUtil.ForEachAura(unit, 'HELPFUL RAID', nil, 298 | function (auraData) 299 | if auraData.duration >= 10*60 then 300 | UpdateTableAura(LBA.state[unit].buffs, auraData) 301 | end 302 | end, 303 | true) 304 | end 305 | end 306 | 307 | local function UpdatePlayerChannel() 308 | LBA.state.player.channel = UnitChannelInfo('player') 309 | end 310 | 311 | local function UpdatePlayerTotems() 312 | LBA.state.player.totems = {} 313 | for i = 1, MAX_TOTEMS do 314 | local exists, name, startTime, duration, model = GetTotemInfo(i) 315 | if exists and name then 316 | if model then 317 | name = LBA.TotemOrGuardianModels[model] or name 318 | end 319 | LBA.state.player.totems[name] = startTime + duration 320 | end 321 | end 322 | end 323 | 324 | local function UpdateUnitInterupt(unit) 325 | local name, endTime, cantInterrupt, _ 326 | 327 | if UnitCanAttack('player', unit) then 328 | name, _, _, _, endTime, _, _, cantInterrupt = UnitCastingInfo(unit) 329 | if name and not cantInterrupt then 330 | LBA.state[unit].interrupt = endTime / 1000 331 | return 332 | end 333 | 334 | name, _, _, _, endTime, _, cantInterrupt = UnitChannelInfo(unit) 335 | if name and not cantInterrupt then 336 | LBA.state[unit].interrupt = endTime / 1000 337 | return 338 | end 339 | end 340 | 341 | LBA.state[unit].interrupt = nil 342 | end 343 | 344 | 345 | --[[------------------------------------------------------------------------]]-- 346 | 347 | function LiteButtonAurasControllerMixin:MarkOverlaysDirty(stateOnly) 348 | -- Tri-state encodes stateOnly : nil / true / false 349 | self.isOverlayDirty = ( stateOnly == true and self.isOverlayDirty ~= false ) 350 | end 351 | 352 | -- Limit UNIT_AURA and UNIT_SPELLCAST overlay updates to one per frame 353 | function LiteButtonAurasControllerMixin:OnUpdate() 354 | if self.isOverlayDirty ~= nil then 355 | self:UpdateAllOverlays(self.isOverlayDirty) 356 | self.isOverlayDirty = nil 357 | end 358 | end 359 | 360 | function LiteButtonAurasControllerMixin:IsTrackedUnit(unit) 361 | if unit == 'player' or unit == 'pet' or unit == 'target' then 362 | return true 363 | else 364 | return false 365 | end 366 | end 367 | 368 | function LiteButtonAurasControllerMixin:OnEvent(event, ...) 369 | if event == 'PLAYER_LOGIN' then 370 | self:Initialize() 371 | self:UnregisterEvent('PLAYER_LOGIN') 372 | self:MarkOverlaysDirty() 373 | return 374 | elseif event == 'PLAYER_ENTERING_WORLD' then 375 | UpdateUnitAuras('target') 376 | UpdateUnitInterupt('target') 377 | UpdateWeaponEnchants() 378 | UpdateUnitAuras('player') 379 | UpdateUnitAuras('pet') 380 | UpdatePlayerChannel() 381 | UpdatePlayerTotems() 382 | self:MarkOverlaysDirty() 383 | elseif event == 'PLAYER_TARGET_CHANGED' then 384 | UpdateUnitAuras('target') 385 | UpdateUnitInterupt('target') 386 | self:MarkOverlaysDirty(true) 387 | elseif event == 'UNIT_AURA' then 388 | -- This fires a lot. Be careful. In DF, UNIT_AURA seems to tick every 389 | -- second for 'player' with no updates 390 | local unit, unitAuraUpdateInfo = ... 391 | if self:IsTrackedUnit(unit) then 392 | UpdateUnitAuras(unit, unitAuraUpdateInfo) 393 | -- Shouldn't be needed but weapon enchant duration is returned 394 | -- wrongly as 0 at PLAYER_LOGIN. This is how Blizzard works around 395 | -- it too. Their server code must be a nightmare. 396 | if unit == 'player' then UpdateWeaponEnchants() end 397 | self:MarkOverlaysDirty(true) 398 | end 399 | elseif event == 'PLAYER_TOTEM_UPDATE' then 400 | UpdatePlayerTotems() 401 | self:MarkOverlaysDirty(true) 402 | elseif event == 'WEAPON_ENCHANT_CHANGED' or event == 'WEAPON_SLOT_CHANGED' then 403 | UpdateWeaponEnchants() 404 | self:MarkOverlaysDirty(true) 405 | elseif event:sub(1, 14) == 'UNIT_SPELLCAST' then 406 | -- This fires a lot too, same applies as UNIT_AURA. 407 | local unit = ... 408 | if unit == 'player' then 409 | UpdatePlayerChannel() 410 | self:MarkOverlaysDirty(true) 411 | elseif self:IsTrackedUnit(unit) then 412 | UpdateUnitInterupt(unit) 413 | self:MarkOverlaysDirty(true) 414 | end 415 | elseif event == 'ITEM_DATA_LOAD_RESULT' then 416 | local itemID, success = ... 417 | if LBA.buttonItemIDs[itemID] then 418 | self:MarkOverlaysDirty() 419 | end 420 | end 421 | end 422 | -------------------------------------------------------------------------------- /Controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |