notes for Classic Era 1.15.9

Patch 1.15.9 (week of 2026-07-19) moved Classic Era / Hardcore / Season of
Discovery onto the shared "Midnight" UI codebase, bringing Edit Mode and the
C_* namespace API migrations that retail did in 11.0.

---------------------------------------------------------------------------------------------------------

ButtonForge.toc

---- Updated with
## Interface: 11509
## Version: 1.0.25-classic-Era

---- Added Compat.lua to the load order, immediately after Declares.lua
     (it must load before EventManager.lua, Util.lua and Button.lua)

---------------------------------------------------------------------------------------------------------

Compat.lua        (new file)

Rebuilds the following, preserving the original calling convention and return
order, on top of whichever API the running client has. Each shim is written as
`<existing global> or <rebuilt version>`, so on an older client the file is a
no-op and behaviour is unchanged.

  Spells        GetSpellInfo, GetSpellSubtext, GetSpellTexture, GetSpellCooldown,
                GetSpellCount, GetSpellLink, IsSpellInRange, IsUsableSpell,
                IsCurrentSpell, IsAutoRepeatSpell, IsAttackSpell, PickupSpell
                                                            -> C_Spell

  Spell book    GetNumSpellTabs, GetSpellTabInfo, GetSpellBookItemInfo,
                GetSpellBookItemName, GetSpellBookItemTexture,
                PickupSpellBookItem, HasPetSpells, BOOKTYPE_SPELL, BOOKTYPE_PET
                                                            -> C_SpellBook

  Items         GetItemInfo, GetItemCount, GetItemCooldown, GetItemIcon,
                IsUsableItem, IsCurrentItem, IsEquippedItem
                                                            -> C_Item

  AddOns        IsAddOnLoaded, LoadAddOn                     -> C_AddOns

  Auras         UnitAura, UnitBuff, UnitDebuff               -> C_UnitAuras

  Action bars   GetActionBarToggles  -- no Edit Mode equivalent; reports live
                                        shown-state of the default multibars

Points worth noting in the shims:
  - IsSpellInRange still returns 1 / 0 / nil, not a boolean. Button.lua tests
    both `if (IsSpellInRange(...))` and `== 1`.
  - GetSpellInfo's second return is the rank. The new table has no rank field,
    so it is filled from GetSpellSubtext.
  - The aura shims rebuild the legacy positional return order. Button.lua and
    Util.lua index by position (icon 2, expirationTime 6, spellId 10).
  - GetSpellBookItemInfo keeps the old single-argument "SpellName(Rank)" lookup
    form that Util.SetCursor uses for Covenant-style spells.
  - Spell book functions accept either the old "spell"/"pet" strings or the new
    Enum.SpellBookSpellBank values.

---------------------------------------------------------------------------------------------------------

Button.lua / Util.lua / EventManager.lua

---- Added a block of file-local aliases near the top of each, pulling the
     needed functions out of BFCompat. Nothing is written to _G, so other
     addons are unaffected, and no call sites needed editing.

---- Button.lua: the existing `local IsUsableSpell = IsUsableSpell;` was
     absorbed into that block.

---------------------------------------------------------------------------------------------------------

Util.lua

---- Util.CreateBlizzardBarWrappers()

     Edit Mode reorganised the default bars, so MultiBar*Button1 / *Button12 may
     be absent. SetPoint against a nil frame throws, and because this runs
     during initialisation it would take the rest of the addon load with it.
     Rewritten as a loop that only anchors a wrapper when both of its anchor
     buttons exist.

     The wrappers stay a dense 1..4 array so that # remains reliable and the
     indices keep lining up with Util.BlizEnabledBars. An unanchored wrapper
     returns nil from GetLeft(), which the snapping code in Util already treats
     as "bar unavailable" and disables.

---------------------------------------------------------------------------------------------------------

Button.lua

---- Button.New() / Button.CreateButtonWidget()

     ActionButtonTemplate's child regions used to be given global names -
     "<button>Icon", "<button>Cooldown", "<button>NormalTexture" and so on.
     On the shared Midnight UI code they are plain members on the button
     instead, so every _G[Name.."..."] lookup returns nil and the first method
     call on one of them throws. Because that happens inside button creation,
     no button ever gets built: the config UI still opens, but adding buttons
     to a bar and dropping actions onto them both silently do nothing.

     Both sites now resolve each region by trying the global name first (so
     older clients behave exactly as before) and falling back to the member
     key - .icon, .cooldown, .Count, .Border, .Flash, .HotKey, .Name,
     .NormalTexture / GetNormalTexture().

     The cooldown setup is guarded, and COOLDOWN_TYPE_NORMAL is read via _G so
     it degrades to nil rather than erroring if it has gone away.

---------------------------------------------------------------------------------------------------------

EventManager.lua        *** this was the root cause ***

     1.15.9 retired the event LEARNED_SPELL_IN_TAB (renamed to
     LEARNED_SPELL_IN_SKILL_LINE in 11.0).

     RegisterEvent throws a hard error on an event name the client doesn't
     know, and EventManager.lua registers ~90 events at file scope. So the
     failure at line 75 aborted the rest of the file. Everything below it
     never ran, in particular:

       line 359   Full:SetScript("OnEvent", Full.InitialOnEvent)
                  -> ADDON_LOADED never handled
                  -> Util.UpdateSavedData() / Util.Load() never called
                  -> ButtonForgeSave stayed nil
                     (UILibLayers.lua:17 and Util.lua:986 errors)

       line 418   function Misc:OnEvent(...)
                  -> never defined
                     (UILibDragIcon.lua:40 and :58 "attempt to call a nil
                      value" when calling MiscFrame:OnEvent)

     Earlier files had already loaded, which is why the toolbar and config
     UI still appeared while nothing that touched saved data worked.

---- All 92 RegisterEvent and 1 UnregisterEvent calls now go through
     BFCompat.RegisterEvent / BFCompat.UnregisterEvent, which map renamed
     events and pcall the call so an unrecognised name can never abort the
     file again. Skipped names are collected and listed by /bfevents.

---- The Misc handler test for LEARNED_SPELL_IN_TAB now uses BFCompat.IsEvent,
     which matches either spelling.

---------------------------------------------------------------------------------------------------------

Button.lua

---- Button:UpdateFlyout()

     ActionButtonTemplate dropped FlyoutBorder and FlyoutBorderShadow
     entirely, and replaced the single FlyoutArrow texture with a
     FlyoutArrowContainer holding FlyoutArrowNormal / Pushed / Highlight.
     All three old members come back nil.

     Because UpdateFlyout runs from ResetAppearance -> SetEnvClear ->
     ClearCommand -> Configure -> Button.New, this fired during the creation
     of every button, so no bar could be populated.

     Rewritten to resolve the regions up front and skip any block whose piece
     is absent. On the modern layout the container is positioned and the
     Normal texture inside it is rotated; on the old layout the single
     texture is used for both, exactly as before. SetClampedTextureRotation
     is called only if it still exists.

     The unguarded SpellFlyout:GetParent() call on the line above (a no-op
     `if` block with its body commented out) was dropped; the same test is
     already made, nil-guarded, on the following line.

---- Button:UpdateGlow()

     ActionButton_ShowOverlayGlow / _HideOverlayGlow were retired in 10.0.
     Now routed through BFCompat.ShowOverlayGlow / HideOverlayGlow, which
     fall back to ActionButtonSpellAlertManager:ShowAlert / :HideAlert.

---------------------------------------------------------------------------------------------------------

Compat.lua / Button.lua / EventManager.lua / Util.lua   -- spell casting

     Symptom: buttons populate and display correctly, bar visibility state
     drivers work, but clicking or keybinding a spell button does nothing and
     produces no Lua error. Silence is expected here - a secure handler that
     rejects a click fails quietly by design.

     Button Forge sets the cast target via SetAttribute("spell", <identifier>)
     and has long used the vanilla "Name(Rank)" string form, because some
     spells misbehaved when cast by numeric ID (see the existing comment above
     the branch: "Patch to fix some spell that doesnt like to be cast with ID
     (Thrash, Stampeding Roar, ...)"). The shared Midnight code resolves spell
     identifiers differently and may not accept a parenthesised rank suffix.

     Which form works can only be settled on a live client, so it is now
     switchable rather than guessed at:

       /bfcast name   "Frostbolt(Rank 3)"   (existing behaviour, the default)
       /bfcast id     116                   (numeric spell ID)
       /bfcast        report current setting

     Change it, then /reload. The choice is stored in
     ButtonForgeGlobalSettings["SpellAttributeMode"] and reloaded during init.

     The default is unchanged, so this alters nothing until deliberately
     switched.

---------------------------------------------------------------------------------------------------------

EventManager.lua        -- ForceOffCastOnKeyDown never applied (pre-existing bug)

     Util.ForceOffCastOnKeyDown is initialised to false in Util.lua:91 and the
     -forceoffcastonkeydown slash command writes
     ButtonForgeGlobalSettings["ForceOffCastOnKeyDown"], but nothing ever
     copied the saved value back into Util. The option therefore did nothing,
     in any client version.

     Now applied during init, after Util.UpdateSavedData() (which creates the
     settings table) and before Util.Load() (which builds the buttons, and
     whose SetupActionButtonClick reads the flag at creation time).

     BFCompat.LoadSpellAttributeMode() was also moved after UpdateSavedData()
     for the same reason - it reads a table that call is responsible for
     creating.

---------------------------------------------------------------------------------------------------------

Compat.lua      -- /bfattr extended

     Also reports KeyBindValue, the ActionButtonUseKeyDown CVar, and the
     effective ForceOffCastOnKeyDown flag, so the click-path configuration can
     be read off in one command.

---------------------------------------------------------------------------------------------------------

Button.lua      *** the cause of "buttons depress but nothing casts" ***

     Button.CreateButtonWidget builds each button from two templates:

       CreateFrame("CheckButton", Name, Parent,
                   "SecureActionButtonTemplate, ActionButtonTemplate")

     On the shared Midnight UI code, ActionButtonTemplate installs its own
     OnClick handler. Because it is listed second, it wins over
     SecureActionButtonTemplate's SecureActionButton_OnClick. Clicks were
     therefore being consumed by Blizzard's action-bar logic, which acts on
     self.action - set to the dummy slot 10000 further down the same function -
     and so did nothing whatsoever.

     That produced the exact observed behaviour: the button shows its pressed
     state, the secure attributes are correct and readable (/bfattr showed
     type = macro, macro = 121), no Lua error is raised, and no action occurs.
     A bare SecureActionButtonTemplate button built by hand cast normally,
     which is what isolated the second template as the difference.

     SecureActionButton_OnClick is now reasserted immediately after creation,
     guarded on inequality so that it is a no-op on clients where
     ActionButtonTemplate installs no OnClick of its own.

---------------------------------------------------------------------------------------------------------

Button.lua      *** the actual cause of "buttons depress but nothing casts" ***

     Since Dragonflight, SecureActionButton_OnClick refuses to process a secure
     action unless the click edge matches the ActionButtonUseKeyDown CVar: a
     down-click when it is on, an up-click when it is off. 1.15.9 brought that
     behaviour to the Classic clients.

     Reference: Stanzilla/WoWUIBugs issue #282, which documents the same
     failure mode - a SecureActionButtonTemplate button registered only for
     "AnyUp" is inoperable while ActionButtonUseKeyDown is 1 (the default),
     with no error.

     Button:SetupActionButtonClick delivered the wrong edge on both paths:

       - With ForceOffCastOnKeyDown it called RegisterForClicks("AnyUp") only,
         so the down-click the handler wanted never arrived.

       - Otherwise it registered both edges, but the wrapper snippet returned
         false on the down-click - suppressing precisely the edge the handler
         wanted - and passed the up-click, which the handler then discarded.

     This also explains why the earlier diagnostics all looked healthy: the
     attributes were correct, SecureActionButton_OnClick was correctly
     installed, and the click reached the button (hence the pressed state).
     The handler ran and declined at its first check.

     Rewritten to register both edges and suppress neither. The handler
     already discards the edge it does not want, so the addon does not need to
     filter. The wrapper's only remaining job is translating the synthetic
     "KeyBind" click into a LeftButton one, which is what the non-key-down
     branch already did.

     Under ForceOffCastOnKeyDown the button now sets useOnKeyDown = false to
     pin itself to acting on release irrespective of the CVar; the attribute
     is ignored by clients that do not know it.

---------------------------------------------------------------------------------------------------------

Button.lua      -- template order in CreateButtonWidget

     The button inherits two templates, and on the shared Midnight UI code
     ActionButtonTemplate installs its own OnClick handler. Inherited scripts
     are applied in the order the templates are listed, so the original

       "SecureActionButtonTemplate, ActionButtonTemplate"

     let the ActionButton handler land last and win, swallowing every click.

     Order swapped to

       "ActionButtonTemplate, SecureActionButtonTemplate"

     so SecureActionButton_OnClick is the winning handler.

     This must be done by template order, not by calling SetScript afterwards.
     An interim build did the latter; it made clicks reach the cast, but a
     script assigned from addon code is tainted, so the action was then refused
     with "ButtonForge has been blocked from an action only available to the
     Blizzard UI". Set by the template, the handler stays secure.

     Sequence of observations that established this:
       - original order, silent failure
       - SetScript reassertion, taint block (so the click now reached the cast)
       - reassertion removed, silent failure again (so the template order is
         what decides which handler runs)

---------------------------------------------------------------------------------------------------------

Button.lua      -- auto-attack flash rendering in the wrong place

     The flash texture (Interface\Buttons\UI-QuickslotRed) used to be anchored
     TOPLEFT/BOTTOMRIGHT to the button by ActionButtonTemplate itself. On the
     current template it is positioned by Blizzard's action button mixin
     instead, which Button Forge never runs - it only ever Show()s and Hide()s
     the texture, relying on the template for placement. The result is a red
     rectangle flashing offset from the button it belongs to.

     Button.New now pins it explicitly, using the same anchors the old template
     applied, so the change is a no-op on clients where the template still
     positions it.

---------------------------------------------------------------------------------------------------------

Button.lua      -- auto-attack flash position

     The flash texture (Interface\Buttons\UI-QuickslotRed) used to be anchored
     TOPLEFT/BOTTOMRIGHT to the button by ActionButtonTemplate itself. On the
     current template it is positioned by Blizzard's action button mixin
     instead, which Button Forge never runs, so it rendered offset from the
     button it belongs to - a red rectangle flickering beside the icon.

     Button.New now pins it explicitly using the same anchors the old template
     applied, so the change is a no-op on clients where the template still
     positions it.

=========================================================================================================
NEW FEATURE (not a 1.15.9 fix)
=========================================================================================================

AuraWatch.lua       -- buff reminder glow

     Pulsing glow on a spell button when the buff that spell grants is not
     currently on the player. Off by default.

     This is distinct from the existing Button:UpdateGlow, which reacts to
     SPELL_ACTIVATION_OVERLAY_GLOW_SHOW/HIDE - server-driven proc highlighting,
     which cannot express "this buff has fallen off". The two do not interact.

     Which spells qualify: glowing every spell with no matching buff would
     light up the entire bar, and a hand-maintained list of every buff in the
     game would rot. Instead it learns - whenever a player buff is seen whose
     name matches a spell on a Button Forge button, that spell name is
     remembered. Casting the buff once teaches it. The learned set is saved in
     ButtonForgeGlobalSettings["AuraWatchSpells"] and is editable by hand.

     A watched button only glows when the spell is currently castable (usable,
     and not on a cooldown longer than the GCD allowance), so it reminds
     rather than nags.

     Glow re-evaluates on a 0.2s throttle and pulses on a 1.2s triangle wave.
     The buff cache refreshes on UNIT_AURA for the player rather than polling.

     /bfglow on|off
     /bfglow list            show learned buff spells
     /bfglow add <spell>     watch a spell by exact name
     /bfglow remove <spell>
     /bfglow clear           forget everything learned
