From 13a69fdb82ca8149c2186128f11fdd1e8f0f26fb Mon Sep 17 00:00:00 2001 From: JuJuFX-dev Date: Mon, 14 Sep 2026 08:49:43 +0200 Subject: [PATCH 1/5] refactor(player-aurabars): render weapon enchants through the engine Weapon oils and imbues never reach an aura container through aura parsing (they carry an enchantID, not a spell ID), so the display was hand-built: a SecureActionButton trio per host, a duplicate style pipeline, its own tooltip, duration ticker and event registration, a cell shift on the container and an alpha-only combat model. 12.1 renders them natively instead (AuraContainer:AddItemEnchantment), and it also made the old design illegal: the centered grow directions had the enchant host ride the container's rect, which now errors with "Anchoring disallowed as dependent object would inherit forbidden aspects: UntrustedLayoutScriptExecution" for any dependent, secure or not. AuraKit gains AddItemEnchantmentsToContainer, SetContainerItemEnchantmentLayout, SetContainerItemEnchantmentSort and HasItemEnchantments. The declared slots are tracked in containerData (HasItemEnchantment is engine-private and a second declaration for the same slot asserts), and ReleaseContainer untracks the returned frames from the restyle registry. Player Aura Bars declares the enchants on the Buffs container as a leading layout group, so the engine flows them ahead of the aura run in every grow direction, keeps doing so in combat, and styles them from the bar's live style; icon, item tooltip and the protected CancelTemporaryEnchantment click are engine-side (the last via style.cancelButtons). The row has no addon-facing unregister, so showWeaponEnchants is folded into the buffs content signature and a flip rides the existing release and rebuild lane. Unit Frames declare the same group on the player buff container; their cells now follow that container's visibility instead of the old All Buffs / Has Duration gate, since UF has no container swap lane. groupSpacing is the gap before the group that continues the line, so the distance to the first aura sits on the aura groups' layout, set in UF only where enchants are actually declared. The options preview models one uniform flow now (enchants as leading members of the same line) instead of the retired wholesale container shift, and EUI_UnitFrames_WeaponEnchants.lua is gone with its toc entry. --- .../EUI_PlayerAuraBars_ManagerPages.lua | 4 +- .../EUI_UnitFrames_AuraContainers.lua | 79 +-- .../EUI_UnitFrames_WeaponEnchants.lua | 507 ------------------ .../EllesmereUIUnitFrames.toc | 1 - .../EllesmereUIUnitFrames_PlayerAuraBars.lua | 254 +++------ EllesmereUI_AuraKit.lua | 80 ++- 6 files changed, 196 insertions(+), 729 deletions(-) delete mode 100644 EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua diff --git a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua index 32038ee3f..441fe5604 100644 --- a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua +++ b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua @@ -328,8 +328,8 @@ local function DefaultBuffBarName(cfg) end -- Weapon-enchants-only is a fundamentally different shape of bar -- at most --- three cells (main hand / off hand / ranged, see EUI_UnitFrames_ --- WeaponEnchants.lua's SLOTS, which matches Blizzard's own +-- three cells (main hand / off hand / ranged, the engine's own +-- AuraContainerItemEnchantmentSlot set, which matches Blizzard's -- UpdateTemporaryEnchantmentBuffs) instead of a wrapping buff grid. Resize the -- grid to fit on the way in and restore it on the way out. -- diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua index 386454742..0b3b43074 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua @@ -1213,11 +1213,9 @@ local function AnchorContainer(container, frame, unit, base, s, buffContainer) local mergedBuff = merged and isBuff if mergedBuff then anchor = s.debuffAnchor end if anchor == "none" then - -- Player buffs hidden: retire the weapon-enchant lead strip too. - if unit == "player" and isBuff and ns._weaponEnchUF then - ns._weaponEnchUF = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end - end + -- Player buffs hidden: the weapon enchants ride this container (see + -- the declaration below), so ApplyGroupConfig's SetShown(false) takes + -- them down with it -- nothing to retire here. return anchor end @@ -1308,36 +1306,38 @@ local function AnchorContainer(container, frame, unit, base, s, buffContainer) container:SetPoint(ia, frame, fp, ox + offX, oy + cbOff + offY) AK.SetContainerAnchor(container, ia) if unit == "player" and isBuff then - -- Weapon enchant lead icons (oils/imbues are not auras; see - -- EUI_UnitFrames_WeaponEnchants.lua): ride the SAME resolved - -- anchor as the player's buff container so the strip leads it. - -- Published only while the broad-content mode admits generic - -- duration buffs (All Buffs or Has Duration -- the catch-all - -- gate) AND the buff display itself is on; renders with the - -- container's live style so customizations follow. - local broad = s.buffShowAll ~= false or s.buffHasDuration == true - local shownBuffs = (s.showBuffs ~= false) - or (s.debuffAnchorBuffs == true and (s.debuffAnchor or "none") ~= "none") - if broad and shownBuffs then - ns._weaponEnchUF = { frame = frame, ia = ia, fp = fp, - x = ox + offX, y = oy + cbOff + offY, gX = gX, - pad = EllesmereUI.PP.FromPixels(s.buffSpacingX or 1), - styleKey = StyleKey("player", "HELPFUL") } - -- Shift the engine run inward past the enchant cells (main - -- hand adjacent to the run; zero enchants = zero shift). - local n = (ns.WeaponEnchants_Count and ns.WeaponEnchants_Count()) or 0 - if n > 0 then - local st = AK.styles[StyleKey("player", "HELPFUL")] - local w = (st and st.width) or 22 - local sign = (gX == "RIGHT") and 1 or -1 - local shift = sign * n * (w + EllesmereUI.PP.FromPixels(s.buffSpacingX or 1)) - container:ClearAllPoints() - container:SetPoint(ia, frame, fp, ox + offX + shift, oy + cbOff + offY) - end - else - ns._weaponEnchUF = nil - end - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + -- Weapon enchants are not auras: the engine's own item- + -- enchantment source renders them as a layout group flowed AHEAD + -- of the aura groups on this container (see + -- AK.AddItemEnchantmentsToContainer), so nothing is anchored and + -- no cells are reserved. Declared lazily and never removed (no + -- addon-facing unregister), so they follow this container's + -- visibility instead of the old broad-content gate. + local PP = EllesmereUI.PP + local w, h = ElementSize(unit, base, s) + local gap = PP.FromPixels(s.buffSpacingX or 1) + local lineGap = PP.FromPixels(s.buffSpacingY or 1) + local placement = CustomAuraContainerItemEnchantmentPlacement + local sortMethods = AuraContainerItemEnchantmentSortMethod + local sortDirs = AuraContainerSortDirection + AK.AddItemEnchantmentsToContainer(container, { + style = StyleKey("player", "HELPFUL"), + hidePermanent = true, + -- REVERSE keeps main hand adjacent to the aura run: the + -- engine puts the group's first element at the leading edge, + -- and Slot order is main hand, off hand, ranged. + sortMethod = sortMethods and sortMethods.Slot, + sortDirection = sortDirs and sortDirs.Reverse, + layout = { + elementWidth = w, + elementHeight = h, + elementSpacing = gap, + lineSpacing = lineGap, + groupSpacing = gap, + groupLineSpacing = lineGap, + placement = placement and placement.BeforeAuraGroups, + }, + }) end end AK.SetContainerGrowth(container, FlowDir(gX), FlowDir(gY)) @@ -1424,7 +1424,14 @@ local function ApplyGroupConfig(container, unit, base, s, chain, declared) cand.excludeSpellIDs = ex end - local layout = { elementWidth = size, elementHeight = h, elementSpacing = spX, lineSpacing = spY } + -- groupSpacing is the gap BEFORE a group that continues the line, so the + -- distance from the leading weapon-enchant group to the first aura has to + -- sit on the AURA groups. Only where enchants are actually declared (the + -- player's buff container): every other element keeps its existing + -- zero inter-group spacing. + local groupGap = AK.HasItemEnchantments(container) and spX or nil + local layout = { elementWidth = size, elementHeight = h, elementSpacing = spX, lineSpacing = spY, + groupSpacing = groupGap } -- Active set = "all" (a non-player BUFF element with no classes enabled) -- or each chain link. Every other declared group -- disabled classes and diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua deleted file mode 100644 index 540f49841..000000000 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua +++ /dev/null @@ -1,507 +0,0 @@ -if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_ClientGate.lua) --- EUI_UnitFrames_WeaponEnchants.lua --- Weapon oils/imbues are temporary weapon ENCHANTS, not auras: nothing with --- a spell ID ever exists on the player (the API struct carries an enchantID, --- an item-enchant identifier), so the engine aura containers behind the --- Player Aura Bars Buffs bar and the player unit frame's buffs can never --- render them -- containers only render auras that exist in the aura stream. --- Blizzard's own BuffFrame hand-injects "TempEnchant" buttons in Lua for the --- same reason (and may read secrets untainted; we cannot copy that path). --- --- Integration model -- first-class citizens of BOTH displays: --- * IN-GRID CELLS: while N enchants are active, the display's engine --- container is SHIFTED inward by N cells (the producers own that shift; --- this file exposes ns.WeaponEnchants_Count and pokes them on change), --- and the enchant buttons occupy the bar's actual first cells -- main --- hand adjacent to the run, Blizzard's temp-enchants-first ordering. --- No oils = zero shift = byte-identical layout. --- * LIVE STYLE: buttons render from the display's own AK.styles table and --- speak both style dialects (PAB BuildStyle and the unit-frame --- BuildStyle: texCoord rect vs iconCrop/iconZoom, cdText* vs duration*, --- stackPos vs stackPoint, noTooltips) -- customizations follow --- automatically. --- * FILTER SEMANTICS: producers publish a record only while their --- broad-content mode admits generic duration buffs (All Buffs OR Has --- Duration on); curated-only configs render and shift nothing. --- * TOOLTIP + CANCEL: the inventory-item tooltip mirrors Blizzard's --- TempEnchant button (GameTooltip:SetInventoryItem -- item data only --- GameTooltip can render, same pattern as Bags). PAB buttons cancel on --- right-click through the SECURE path: CancelTemporaryEnchantment is --- hard-protected for addon code (ADDON_ACTION_FORBIDDEN), so the PAB --- trio are SecureActionButtons with type2="cancelaura" + --- target-slot2=, which SECURE_ACTIONS.cancelaura resolves --- to the protected call engine-side. UF buttons never cancel (spec). --- --- COMBAT MODEL (the secure trio cannot be moved/shown/hidden/re-attributed --- in lockdown): each PAB button is SLOT-FIXED (attributes set once at --- creation), pre-warmed shown at alpha 0 out of combat, and in-combat --- transitions touch only alpha + content (icon/cooldown/texts -- all legal --- on protected frames). Geometry, mouse flags and the producers' container --- shifts are deferred to PLAYER_REGEN_ENABLED, then one full pass repacks. --- --- Secrecy: GetTemporaryEnchantmentInfo's returns carry NO Secret* flags in --- the generated API docs (equipment state, not unit-aura data) -- plain --- numbers in restricted combat too. --- --- Cost model: WEAPON_ENCHANT_CHANGED / WEAPON_SLOT_CHANGED (the pair --- Blizzard's BuffFrame registers) are the only wake-ups, registered only --- while a display publishes a record; the duration text runs on a shared --- 0.5s ticker that exists ONLY while an enchant button is visible. --- --- Producer records (nil = display inactive/filtered), poked via --- ns.WeaponEnchants_Layout(): --- ns._weaponEnchPAB = { parent, corner, dir, pad, styleKey, canCancel } --- ns._weaponEnchUF = { frame, ia, fp, x, y, gX, pad, styleKey } - -local _, ns = ... -local EllesmereUI = _G.EllesmereUI - -local SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } - -local hosts = {} -- "pab"/"uf" -> { frame, buttons = {}, rec } -local evFrame, regenFrame, textTicker -local activeInfos, activeCount = {}, 0 -- packed active list (MH first) -local activeBySlot = {} -- slot -> info - --- Raw active-enchant count. Producers gate on their own record/filters and --- shift their container inward by this many cells. -function ns.WeaponEnchants_Count() - return activeCount -end - -local function Style(rec) - local AK = EllesmereUI.AuraKit - return (AK and AK.styles and rec and AK.styles[rec.styleKey]) or nil -end - -local function OnButtonEnter(self) - if self._noTooltip or not self._slot then return end - GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT") - GameTooltip:SetInventoryItem("player", self._slot) -end - -local function OnButtonLeave() - GameTooltip:Hide() -end - --- host.secure: SecureActionButton trio, one per SLOT, cancel wired once at --- creation (attribute writes are combat-blocked, so nothing is ever --- re-attributed). Insecure hosts get plain frames. -local function MakeButton(host, index) - local b - if host.secure then - b = CreateFrame("Button", nil, host.frame, "SecureActionButtonTemplate") - b:SetAttribute("type2", "cancelaura") - b:SetAttribute("target-slot2", SLOTS[index]) - -- BOTH phases: with the press-and-hold cvar (ActionButtonUseKeyDown, - -- default on) the secure handler executes on the DOWN click and - -- ignores the up -- an up-only registration never fires at all. The - -- handler runs exactly one cvar-selected phase, so no double-fire. - b:RegisterForClicks("RightButtonDown", "RightButtonUp") - else - b = CreateFrame("Frame", nil, host.frame) - end - local icon = b:CreateTexture(nil, "ARTWORK") - icon:SetAllPoints() - b.icon = icon - local cd = CreateFrame("Cooldown", nil, b, "CooldownFrameTemplate") - cd:SetAllPoints() - cd:SetHideCountdownNumbers(true) -- duration text is ours, styled below - b.cd = cd - -- Text rides its own frame ABOVE the cooldown: FontStrings on the button - -- itself render UNDER the Cooldown child frame's swipe. - local tf = CreateFrame("Frame", nil, b) - tf:SetAllPoints() - tf:SetFrameLevel(cd:GetFrameLevel() + 5) - b.duration = tf:CreateFontString(nil, "OVERLAY") - b.charges = tf:CreateFontString(nil, "OVERLAY") - b:SetScript("OnEnter", OnButtonEnter) - b:SetScript("OnLeave", OnButtonLeave) - return b -end - --- Duration swipe gate. SetShown alone does not stick: PaintContent's --- SetCooldown implicitly re-Shows the frame (documented in AK's --- ApplyStyleToRegions), and a cooldown started while it was hidden comes back --- as a swipe that never advances -- a frozen wedge on some passes, nothing on --- others. SetDrawSwipe is persistent cooldown STYLE (SetCooldown never resets --- it, AllowedWhenTainted) and alpha survives any re-show; the texts ride their --- own frame above the cooldown, so the alpha misses them. Show/Hide stays in --- ApplyStyle: it is combat-blocked on the secure trio's descendants. -local function ApplySwipeGate(b, hide) - if b._hideSwipe == hide then return end - b._hideSwipe = hide - if b.cd.SetDrawSwipe then b.cd:SetDrawSwipe(not hide) end - b.cd:SetAlpha(hide and 0 or 1) -end - --- Mirrors the display's live style onto one of our buttons; the texcoord --- cascade is AK ApplyStyleToRegions' exact order. Called out of combat only --- for the secure trio (SetSize is geometry). -local function ApplyStyle(b, style) - local w = style.width or 32 - b:SetSize(w, style.height or w) - local tc = style.texCoord - if tc then - b.icon:SetTexCoord(tc[1], tc[2], tc[3], tc[4]) - elseif style.iconCrop then - local z = style.iconZoom or 0.07 - b.icon:SetTexCoord(z, 1 - z, z, 1 - z) - else - b.icon:SetTexCoord(0, 1, 0, 1) - end - local PP = EllesmereUI.PP - local bd = style.border - if bd and PP then - if not b._bdr then - b._bdr = CreateFrame("Frame", nil, b) - b._bdr:SetAllPoints() - b._bdr:EnableMouse(false) - end - b._bdr:SetFrameLevel(bd.behind - and math.max(0, b:GetFrameLevel() - 1) - or (b.cd:GetFrameLevel() + 1)) - EllesmereUI.ApplyBorderStyle(b._bdr, bd.textureSize or bd.size or 1, - bd[1] or 0, bd[2] or 0, bd[3] or 0, bd[4] or 1, - bd.texture or "solid", bd.offsetX, bd.offsetY, bd.shiftX, bd.shiftY, - bd.addonKey or "unitframes", bd.sizeKey or bd.size or 1) - elseif b._bdr then - if EllesmereUI.HideBorderStyle then EllesmereUI.HideBorderStyle(b._bdr) end - b._bdr:Hide() - end - b.cd:SetReverse(style.cooldownReverse ~= false) - b.cd:SetDrawEdge(style.cooldownDrawEdge == true) - local hideSwipe = style.hideSwipe == true - ApplySwipeGate(b, hideSwipe) - b.cd:SetShown(not hideSwipe) - - local path = style.fontPath or STANDARD_TEXT_FONT - local flag = style.fontFlag or "OUTLINE" - if style.hideDurationText then - b.duration:Hide() - else - if EllesmereUI.PrimeFontShadow then EllesmereUI.PrimeFontShadow(b.duration, flag == "") end - b.duration:SetFont(path, style.durationFontSize or style.cdTextSize or 11, flag) - b.duration:ClearAllPoints() - b.duration:SetPoint(style.durationPoint or "CENTER", b, - style.durationRelPoint or "CENTER", - style.durationX or style.cdOffX or 0, - style.durationY or style.cdOffY or 0) - local c = style.durationColor or style.cdTextColor - b.duration:SetTextColor(c and c.r or 1, c and c.g or 1, c and c.b or 1) - b.duration:Show() - end - if EllesmereUI.PrimeFontShadow then EllesmereUI.PrimeFontShadow(b.charges, flag == "") end - b.charges:SetFont(path, style.stackFontSize or style.stackSize or 11, flag) - b.charges:ClearAllPoints() - local sp = style.stackPoint or style.stackPos or "BOTTOMRIGHT" - b.charges:SetPoint(sp, b, sp, - style.stackX or style.stackOffX or 0, - style.stackY or style.stackOffY or 0) - local sc = style.stackColor - b.charges:SetTextColor(sc and sc.r or 1, sc and sc.g or 1, sc and sc.b or 1) -end - --- Compact house duration format matching the engine aura text: bare seconds --- under a minute (with the unit when the style's Show S for Seconds is on), --- m/h above. -local function FormatRemaining(sec, showS) - if sec >= 3600 then return string.format("%dh", math.floor(sec / 3600 + 0.5)) end - if sec >= 60 then return string.format("%dm", math.floor(sec / 60 + 0.5)) end - return string.format(showS and "%ds" or "%d", math.floor(sec + 0.5)) -end - -local function UpdateTexts() - local now = GetTime() - local anyShown = false - for _, host in pairs(hosts) do - local st = Style(host.rec) - local showS = st and st.durationShowSeconds - for _, b in pairs(host.buttons) do - if b:IsShown() and b:GetAlpha() > 0 then - anyShown = true - if b._expire and b.duration:IsShown() then - local rem = b._expire - now - if rem > 0 then - b.duration:SetText(FormatRemaining(rem, showS)) - else - b.duration:SetText("") - end - end - end - end - end - if not anyShown and textTicker then - textTicker:Cancel() - textTicker = nil - end -end - -local function EnsureTicker(want) - if want and not textTicker then - textTicker = C_Timer.NewTicker(0.5, UpdateTexts) - elseif not want and textTicker then - textTicker:Cancel() - textTicker = nil - end -end - -local function ReadEnchants() - local n = 0 - for k in pairs(activeBySlot) do activeBySlot[k] = nil end - for i = 1, #SLOTS do - local info = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo - and C_PaperDollInfo.GetTemporaryEnchantmentInfo(SLOTS[i]) - if info and info.hasExpirationTime then - n = n + 1 - activeInfos[n] = { slot = SLOTS[i], slotIndex = i, info = info } - activeBySlot[SLOTS[i]] = info - end - end - for i = n + 1, #activeInfos do activeInfos[i] = nil end - activeCount = n -end - --- Fills one button's CONTENT (legal on protected frames in combat). -local function PaintContent(b, slot, info, hideSwipe) - b.icon:SetTexture(GetInventoryItemTexture("player", slot)) - local remaining = (info.remainingTimeMs or 0) / 1000 - b._expire = GetTime() + remaining - b.cd:SetCooldown(GetTime(), remaining) - -- Combat-legal half of the gate: ApplyStyle cannot run for the secure - -- trio in lockdown, so a swipe toggle flipped there lands here. - ApplySwipeGate(b, hideSwipe) - local ch = info.chargesRemaining or 0 - if ch > 1 then b.charges:SetText(ch); b.charges:Show() - else b.charges:Hide() end - b._slot = slot -end - -local function Paint() - local n = activeCount - local combat = InCombatLockdown() - local anyShown = false - for _, host in pairs(hosts) do - local rec = host.rec - local style = Style(rec) - if host.secure then - -- Slot-fixed secure trio. The host frame and every button stay - -- PERMANENTLY SHOWN once created (Show/Hide on the buttons OR on - -- any ancestor of them is combat-blocked); visibility rides - -- button alpha exclusively. Out of combat: full geometry/style - -- pass; in combat: alpha + content only. Cells pack active slots - -- with MAIN HAND adjacent to the engine run. - local active = rec and style or nil - local cellIndex = {} - if active then - local ci = n - 1 - for i = 1, #SLOTS do - if activeBySlot[SLOTS[i]] then - cellIndex[i] = ci - ci = ci - 1 - end - end - end - local usedCells = {} - for i = 1, #SLOTS do - local b = host.buttons[i] - if not b and not combat and active then - b = MakeButton(host, i) - b:Show() -- pre-warmed forever; visibility rides alpha - host.buttons[i] = b - end - if b then - local info = active and activeBySlot[SLOTS[i]] or nil - if not combat then - if active then - local w = style.width or 32 - local cell = w + (rec.pad or 0) - ApplyStyle(b, style) - local idx = cellIndex[i] or 0 - local dir = rec.dir or "LEFT" - local dx, dy = 0, 0 - if dir == "RIGHT" then dx = 1 elseif dir == "LEFT" then dx = -1 - elseif dir == "UP" then dy = 1 elseif dir == "DOWN" then dy = -1 end - b:ClearAllPoints() - local point = rec.point or rec.corner - b:SetPoint(point, rec.anchorTo or rec.parent, rec.relativePoint or rec.corner or point, - (rec.x or 0) + dx * idx * cell, - (rec.y or 0) + dy * idx * cell) - b._cell = idx - b._noTooltip = style.noTooltips == true - end - b:EnableMouse(info and true or false) - end - if info then - PaintContent(b, SLOTS[i], info, style.hideSwipe == true) - -- In combat positions are frozen: a button whose - -- last-packed cell now belongs to the shifted engine - -- run (or to another enchant) goes alpha 0 instead - -- of overlapping; the regen pass repacks everything. - local cellBad = combat and (b._cell == nil - or b._cell > n - 1 or usedCells[b._cell]) - if cellBad then - b:SetAlpha(0) - else - if combat then usedCells[b._cell] = true end - b:SetAlpha(1) - anyShown = true - end - else - b:SetAlpha(0) - end - end - end - elseif rec and style and n > 0 then - -- Insecure host (UF): packed dynamic buttons, plain frames. - local w = style.width or 32 - local cell = w + (rec.pad or 0) - for i = 1, n do - local b = host.buttons[i] - if not b then b = MakeButton(host, i); host.buttons[i] = b end - ApplyStyle(b, style) - local idx = n - i -- main hand (i=1) adjacent to the run - local sign = (rec.gX == "RIGHT") and 1 or -1 - b:ClearAllPoints() - b:SetPoint(rec.ia, rec.frame, rec.fp, - rec.x + sign * idx * cell, rec.y) - PaintContent(b, activeInfos[i].slot, activeInfos[i].info, - style.hideSwipe == true) - b._noTooltip = style.noTooltips == true - b:EnableMouse(not b._noTooltip) - b:SetAlpha(1) - b:Show() - anyShown = true - end - for i = n + 1, #host.buttons do host.buttons[i]:Hide() end - host.frame:Show() - elseif host.frame then - host.frame:Hide() - end - end - EnsureTicker(anyShown) - if anyShown then UpdateTexts() end -end - --- Count changes re-anchor the displays (their containers shift inward by --- the new count). Container re-seating is combat-legal and runs IMMEDIATELY --- (the engine run repositions live); only the SECURE trio's geometry is --- lockdown-blocked, so in combat PAB takes the minimal container-only path --- (the full ApplyLiveConfig would trip the anchor-poisoned parent:SetSize) --- and a regen pass repacks the buttons afterward. -local function PokeProducers(combat) - if combat then - if ns.PAB_ReShiftEnchants then ns.PAB_ReShiftEnchants() end - else - if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end - end - if ns.UF_ReloadAllAuraContainers then ns.UF_ReloadAllAuraContainers() end -end - -local function OnRegen(self) - self:UnregisterEvent("PLAYER_REGEN_ENABLED") - ReadEnchants() - PokeProducers(false) - Paint() -end - -local function OnEnchantEvent() - local prev = activeCount - ReadEnchants() - if activeCount ~= prev then - local combat = InCombatLockdown() - PokeProducers(combat) - if combat then - -- Secure-button geometry can't follow until regen: repack then. - if not regenFrame then - regenFrame = CreateFrame("Frame") - regenFrame:SetScript("OnEvent", OnRegen) - end - regenFrame:RegisterEvent("PLAYER_REGEN_ENABLED") - end - end - Paint() -end - -local function LayoutHost(key, rec) - local host = hosts[key] - if not rec then - -- Secure host: never Hide (protected descendants make even the - -- insecure ancestor's Hide combat-blocked, and the always-shown - -- model needs no visibility ops at all -- buttons go alpha 0). - if host then - host.rec = nil - if not host.secure then host.frame:Hide() end - end - return - end - if not host then - host = { frame = CreateFrame("Frame", nil, UIParent), buttons = {}, - secure = (key == "pab") } - host.frame:Show() - hosts[key] = host - end - host.rec = rec - if host.secure and InCombatLockdown() then return end - -- Parent from rec.parent ONLY -- rec.anchorTo can be an engine aura - -- container, which carries forbidden aspects - -- (UntrustedLayoutScriptExecution): SetParent into it hard-errors for - -- the secure host ("child object would inherit forbidden aspects"). - -- Anchoring is legal, so the host RIDES the container's rect while - -- living in the plain bar frame's tree. - local function DoAnchor() - local r = host.rec - -- Cleared-rec guard, plus the outer skip mirrored: AuraKit jobs are - -- combat-runnable, so a job enqueued OOC can drain in lockdown and - -- these writes on the SECURE host would be blocked actions there. - -- The rec stays, and the regen relayout heals it like the old skip. - if not r or (host.secure and InCombatLockdown()) then return end - host.frame:SetParent(r.parent or r.frame) - host.frame:ClearAllPoints() - host.frame:SetAllPoints(r.anchorTo or r.parent or r.frame) - end - if host.secure then - -- Some callers (e.g. Unlock Mode's grow-direction dropdown) reach - -- here from a live, insecure OnClick handler; anchoring the secure - -- host straight onto an engine aura container from that stack throws - -- UntrustedLayoutScriptExecution. AuraKit's own job queue gives the - -- anchor write a fresh, untainted execution context instead. - local AK = EllesmereUI.AuraKit - if AK and AK.QueueLiveBuildJob then - AK.QueueLiveBuildJob(DoAnchor, "uf:weaponench-anchor") - else - DoAnchor() - end - else - DoAnchor() - end -end - -local function EnsureEvents() - local want = false - for _, host in pairs(hosts) do - if host.rec then want = true; break end - end - if want then - if not evFrame then - evFrame = CreateFrame("Frame") - evFrame:SetScript("OnEvent", OnEnchantEvent) - end - evFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") - evFrame:RegisterEvent("WEAPON_SLOT_CHANGED") - elseif evFrame then - evFrame:UnregisterAllEvents() - end -end - -function ns.WeaponEnchants_Layout() - LayoutHost("pab", ns._weaponEnchPAB) - LayoutHost("uf", ns._weaponEnchUF) - EnsureEvents() - ReadEnchants() - Paint() -end - --- Producers that loaded (and published) before this file get picked up now; --- later publishes arrive via their own Layout pokes. -ns.WeaponEnchants_Layout() diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc index a49620c86..80c38b23b 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc @@ -14,6 +14,5 @@ EUI_UnitFrames_Engine.lua EllesmereUIUnitFrames.lua EllesmereUIUnitFrames_PlayerAuraBars.lua EUI_UnitFrames_AuraContainers.lua -EUI_UnitFrames_WeaponEnchants.lua # Options diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua index 1707c200b..7320ece44 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua @@ -2329,85 +2329,45 @@ local function SyncNativeAuras() end end --- Shifts the default Buffs container inward by the active weapon-enchant --- count (see EUI_UnitFrames_WeaponEnchants.lua): the enchant buttons occupy --- the bar's first cells and the engine run starts after them -- Blizzard's --- temp-enchants-first ordering. Zero enchants (or a filtered-out record) --- leaves the anchor byte-identical. Full rows overflow the reserved grid by --- the shift while an oil is up -- accepted; the shift is transient. -local function ShiftBuffsForEnchants(container, parent, cfg, grid) - local n = (cfg.showWeaponEnchants == true and ns.WeaponEnchants_Count and ns.WeaponEnchants_Count()) or 0 - local containerAnchor = BuildContainerSpec(parent, cfg, grid) - local dir = cfg.growDirection or "LEFT" - local cell = PabSnap(cfg.iconSize or 32) + PabSnap(cfg.padding or 5) - container:ClearAllPoints() - -- Centered modes: the enchant cells must hug the RUN's moving edge, which - -- only the container's live rect knows. rec.parent must stay the PLAIN bar - -- frame -- the container carries forbidden aspects - -- (UntrustedLayoutScriptExecution), and the consumer's secure host frame - -- hard-errors on SetParent into that subtree ("child object would inherit - -- forbidden aspects"). rec.anchorTo carries the container for ANCHORING - -- only (SetPoint relative-to does not reparent), which is the same trust - -- shape as the buttons' existing anchors into insecurely-positioned frames. - if dir == "CENTER_HORIZONTAL" then - local span = n * cell - container:SetPoint("CENTER", parent, "CENTER", span / 2, 0) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = container - ns._weaponEnchPAB.corner = nil - ns._weaponEnchPAB.point = "LEFT" - ns._weaponEnchPAB.relativePoint = "LEFT" - ns._weaponEnchPAB.x = -span - ns._weaponEnchPAB.y = 0 - ns._weaponEnchPAB.dir = "RIGHT" - end - return - end - if dir == "CENTER_VERTICAL" then - local span = n * cell - container:SetPoint("CENTER", parent, "CENTER", 0, -span / 2) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = container - ns._weaponEnchPAB.corner = nil - ns._weaponEnchPAB.point = "BOTTOM" - ns._weaponEnchPAB.relativePoint = "TOP" - ns._weaponEnchPAB.x = 0 - ns._weaponEnchPAB.y = math.max(0, n - 1) * cell + PabSnap(cfg.padding or 5) - ns._weaponEnchPAB.dir = "DOWN" - end - return - end +-- Weapon enchants are not auras, so only the engine's own item-enchantment +-- source renders them (see AK.AddItemEnchantmentsToContainer): a LEADING +-- layout group on the Buffs container, flowed ahead of the aura run in every +-- grow direction and in combat. Opt-in per bar (showWeaponEnchants, the +-- pinned "Weapon Enchants" Filters row), a content source of its own and so +-- never gated on the broad-content modes. +local function BuildEnchantSpec(cfg, pad, rowGap) + local layout = BuildGroupLayout(cfg, pad, rowGap) + local placement = CustomAuraContainerItemEnchantmentPlacement + if placement then layout.placement = placement.BeforeAuraGroups end + local sortMethods = AuraContainerItemEnchantmentSortMethod + local sortDirs = AuraContainerSortDirection + return { + style = STYLE_BUFFS, + layout = layout, + hidePermanent = true, + -- REVERSE keeps MAIN HAND adjacent to the aura run: the engine puts + -- the group's first element at the leading edge, and Slot order is + -- main hand, off hand, ranged. + sortMethod = sortMethods and sortMethods.Slot, + sortDirection = sortDirs and sortDirs.Reverse, + } +end - local dx, dy = 0, 0 - if dir == "RIGHT" then dx = 1 elseif dir == "LEFT" then dx = -1 - elseif dir == "UP" then dy = 1 elseif dir == "DOWN" then dy = -1 end - container:SetPoint(containerAnchor, parent, containerAnchor, dx * n * cell, dy * n * cell) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = nil - ns._weaponEnchPAB.corner = containerAnchor - ns._weaponEnchPAB.point = nil - ns._weaponEnchPAB.relativePoint = nil - ns._weaponEnchPAB.x = nil - ns._weaponEnchPAB.y = nil - ns._weaponEnchPAB.dir = dir - end -end - --- Combat-path re-shift for enchant count changes: re-seating the CONTAINER --- is combat-legal (plain SetPoint, same class as the merged-debuff ride), --- but the full ApplyLiveConfig is not -- the secure enchant trio anchors --- into the bar frame's family, which blocks the bar's own SetSize in --- lockdown. Recomputes the live grid and re-seats ONLY the container, --- INCLUDING the shift-to-zero reset when the last oil expires. -function ns.PAB_ReShiftEnchants() - local s = PAB() - if not (AK and s and buffsContainer and buffsParent) then return end - local cfg = DefaultBuffsCfg(s) - local grid = ComputeGrid(true, cfg) - ShiftBuffsForEnchants(buffsContainer, buffsParent, cfg, grid) +-- Declares them on `container` (idempotent) and re-applies their layout, so +-- a live padding/icon-size change follows. Turning the row OFF is served by +-- the content signature below instead: the engine has no addon-facing +-- unregister, so the container has to be rebuilt for that. +local function ApplyEnchants(container, cfg, pad, rowGap) + if not (container and cfg and cfg.showWeaponEnchants == true) then return end + AK.AddItemEnchantmentsToContainer(container, BuildEnchantSpec(cfg, pad, rowGap)) +end + +-- Buffs content signature: the resolved spell set PLUS the weapon-enchant +-- row. A group's candidateFilters are fixed at declaration and an item +-- enchantment cannot be undeclared at all, so a change in either releases +-- the container and builds a fresh one. +local function BuffsContentSig(cfg, spells) + return table.concat(spells, ",") .. (cfg.showWeaponEnchants == true and "|e" or "") end local function CreateBars() @@ -2506,31 +2466,9 @@ local function CreateBars() local buffPad = buffCfg.padding or 5 local debuffPad = debuffCfg.padding or 5 - local buffCorner, buffSpec = BuildContainerSpec(buffsParent, buffCfg, buffGrid) + local _, buffSpec = BuildContainerSpec(buffsParent, buffCfg, buffGrid) local _, debuffSpec = BuildContainerSpec(debuffsParent, debuffCfg, debuffGrid) - -- Weapon enchant lead icons (oils/imbues are not auras; see - -- EUI_UnitFrames_WeaponEnchants.lua): opt-in (showWeaponEnchants, default - -- off -- the cell shift offsets the aura grid), exposed as the "Weapon - -- Enchants" pinned row in the bar's Filters dropdown. A content source of - -- its own, NOT gated on the broad-content modes: enchants are not auras - -- and never come from the catch-all group, so checking the row alone - -- shows just the enchant cells. They render with the bar's live style, so - -- every customization follows automatically. - if buffCfg.showWeaponEnchants == true and buffCfg.enabled ~= false then - ns._weaponEnchPAB = { parent = buffsParent, corner = buffCorner, - dir = buffCfg.growDirection or "LEFT", - -- Snapped like the shift's own cell stride above: the buttons add - -- this to an already-snapped style.width, so a raw gap would place - -- them off the engine's grid at a non-native UI scale. - pad = PabSnap(buffPad), styleKey = STYLE_BUFFS, canCancel = true } - else - ns._weaponEnchPAB = nil - end - if buffCfg.growDirection ~= "CENTER_HORIZONTAL" and buffCfg.growDirection ~= "CENTER_VERTICAL" and ns.WeaponEnchants_Layout then - ns.WeaponEnchants_Layout() - end - -- Groups are declared additively right after creation (not via spec.groups) so -- the same ApplyGroupConfig path handles both initial creation and every later -- live settings change. @@ -2557,12 +2495,11 @@ local function CreateBars() -- the sig-diffing. local buffAllChain = BuffBarChain(buffCfg) local buffSpells = ns.PAB_ResolveSpells(buffCfg) - buffsSlotSig = table.concat(buffSpells, ",") + buffsSlotSig = BuffsContentSig(buffCfg, buffSpells) AK.RequestContainer(buffsParent, "player", buffSpec, function(container) buffsContainer = container ApplyContainerAnchorAndGrowth(container, buffsParent, buffCfg, buffGrid) - ShiftBuffsForEnchants(container, buffsParent, buffCfg, buffGrid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + ApplyEnchants(container, buffCfg, buffPad, buffGrid.rowGap) declared.buffs = {} ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, buffGrid.effectiveMax, buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) if #buffSpells > 0 then @@ -2745,12 +2682,6 @@ local function RestyleBars() AK.styles[STYLE_DEBUFFS] = BuildStyle(false, DefaultDebuffsCfg(s)) AK.RestyleSoon(STYLE_BUFFS) AK.RestyleSoon(STYLE_DEBUFFS) - -- RestyleSoon only reaches ENGINE buttons. The weapon-enchant cells - -- carry the bar's style too but repaint only from their own Paint, so - -- the callers that restyle without ApplyLiveConfig (global font/outline - -- changes, profile and spec-override swaps through the - -- _EUF_ReloadFrames tail) would leave them on the previous style. - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end SyncCancelCVar() end ns.PAB_Restyle = RestyleBars @@ -2842,17 +2773,13 @@ local function ApplyLiveConfig(isBuff) local cfg = isBuff and DefaultBuffsCfg(s) or DefaultDebuffsCfg(s) -- Enable toggle: a disabled bar hides its parent and skips every live - -- apply below (geometry, enchant publish, group work) -- re-enabling runs + -- apply below (geometry, enchant layout, group work) -- re-enabling runs -- the full pass. Same shape as the custom bars' early return. Use -- Blizzard Buffs and the MASTER disable stand the default bars down the - -- same way (weapon-enchant events still reach this while disabled -- - -- their registration outlives the module). + -- same way. Weapon enchants ride the container, so they stand down with + -- the bar's parent -- nothing extra to tear down here. ApplyDefaultBarShown(isBuff) if s.enabled ~= true or cfg.enabled == false or s.useBlizzardBuffs == true then - if isBuff then - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end - end return end local grid = ComputeGrid(isBuff, cfg) @@ -2925,20 +2852,10 @@ local function ApplyLiveConfig(isBuff) ApplyContainerAnchorAndGrowth(container, parent, cfg, grid) if isBuff then - -- Keep the weapon-enchant cells riding the bar's live geometry and - -- filter state (opt-in only -- an independent content source, see the - -- publish in CreateBars), then shift the engine run inward past them. - if cfg.showWeaponEnchants == true then - local liveCorner = BuildContainerSpec(parent, cfg, grid) - ns._weaponEnchPAB = { parent = parent, corner = liveCorner, - dir = cfg.growDirection or "LEFT", - -- Snapped, as in CreateBars' publish above. - pad = PabSnap(pad), styleKey = STYLE_BUFFS, canCancel = true } - else - ns._weaponEnchPAB = nil - end - ShiftBuffsForEnchants(container, parent, cfg, grid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + -- Re-applies the enchant group's layout so a live padding/icon-size + -- change follows; the row going OFF rebuilds the container through + -- the signature below instead. + ApplyEnchants(container, cfg, pad, grid.rowGap) -- Unlock mode bakes the mover's label at registration, so a Filters -- change would leave the old name on it until the next CreateBars. @@ -2952,7 +2869,7 @@ local function ApplyLiveConfig(isBuff) if isBuff then local spells = ns.PAB_ResolveSpells(cfg) - local sig = table.concat(spells, ",") + local sig = BuffsContentSig(cfg, spells) local allChain = BuffBarChain(cfg) if sig ~= buffsSlotSig then -- Safe to fully release+rebuild: the default Buffs container holds only the @@ -2960,14 +2877,14 @@ local function ApplyLiveConfig(isBuff) -- container's anchor/growth/rowWidth come from `spec` below -- the live -- SetContainerAnchor/etc calls above ran against the OLD container and are -- harmless overhead. A group's candidateFilters is fixed at declaration, so a - -- spell-list change requires this release+rebuild. + -- spell-list change requires this release+rebuild -- and so does the + -- weapon-enchant row, which the engine cannot undeclare at all. RetireContainer(container, declared.buffs) local _, spec = BuildContainerSpec(parent, cfg, grid) AK.RequestContainer(parent, "player", spec, function(newContainer) buffsContainer = newContainer ApplyContainerAnchorAndGrowth(newContainer, parent, cfg, grid) - ShiftBuffsForEnchants(newContainer, parent, cfg, grid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + ApplyEnchants(newContainer, cfg, pad, grid.rowGap) declared.buffs = {} ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if #spells > 0 then @@ -4576,11 +4493,11 @@ local function PreviewSpellIcon(spellID) end -- Weapon-enchant preview cells, leading the bar exactly like the live ones --- (EUI_UnitFrames_WeaponEnchants.lua publishes them ahead of the engine run). +-- (the engine flows its item-enchantment group ahead of the aura groups). -- Main hand + off hand only: those are the two slots reachable in current --- retail content, even though both that module's SLOTS and Blizzard's --- UpdateTemporaryEnchantmentBuffs still poll a third (ranged) -- so a bar sized --- for three enchants shows one placeholder cell of genuine spare capacity here, +-- retail content, even though the engine declares a third (ranged), as +-- Blizzard's own UpdateTemporaryEnchantmentBuffs does -- so a bar sized for +-- three enchants shows one placeholder cell of genuine spare capacity here, -- which is what the live bar would do too. -- -- Paints the player's OWN equipped weapon icons rather than an invented sample: @@ -4997,18 +4914,18 @@ local function BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- independent mixed order FIRST, then sort that fixed selection for display. -- Weapon enchants take the LEADING cells and are never sorted into the aura -- content: they are not auras. They are also ADDITIVE, not a slice of the - -- bar's capacity -- the live container keeps its full maxFrameCount and is - -- shifted past them wholesale (ShiftBuffsForEnchants), so an enchant never - -- costs an aura its slot. + -- bar's capacity -- the live container keeps its full maxFrameCount and the + -- engine flows the enchant group ahead of the aura groups, so an enchant + -- never costs an aura its slot (it does share the line, so the wrap moves). local numEnch, enchSlots = 0, nil if isBuff and cfg.showWeaponEnchants == true then enchSlots = PreviewEnchantSlots() numEnch = #enchSlots end -- The two modes are genuinely different shapes and the preview mirrors both: - -- * alongside auras -- the container keeps its full maxFrameCount and is - -- shifted past the enchants wholesale, so they cost no aura its slot and - -- the first row overflows the reserved grid by the shift. + -- * alongside auras -- the container keeps its full maxFrameCount and the + -- enchant group leads the flow, so they cost no aura its slot but do + -- share the line: a full row wraps that many icons further down. -- * enchants-only -- the grid was auto-sized FOR the enchants -- (SyncWeaponEnchantsGrid) and the container holds no groups, so the -- cells sit INSIDE that reserved width. The leftover cells stay as @@ -5186,11 +5103,12 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) -- selects the fixed filler slice from this stable, shuffled-once order FIRST, sorts after. local list = (pool and #pool > 0 and pool) or (isBuff and PREVIEW_BUFF_SPELLS or PREVIEW_DEBUFF_SPELLS) local listLen = #list - local slots, numEnch = BuildPreviewSlots(isBuff, cfg, list, listLen, count) + -- Enchant cells are the leading `slots` entries and carry their own kind, + -- so the packing below needs no separate count of them. + local slots = BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- Enchants are additive leading cells, so the rendered total exceeds the -- bar's aura capacity by however many are showing. local total = #slots - local auraCount = total - numEnch -- Icon Effects Per-Filter preview (debuffs only): deliberately NOT tied to the -- bar's own active Base Filters/Show All Debuffs state -- requiring a matching @@ -5222,7 +5140,9 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) end end - local rows = math.max(1, math.ceil(auraCount / cols)) + -- Enchant cells share the line with the auras (one engine flow, see the + -- packing block below), so they count toward the wrap. + local rows = math.max(1, math.ceil(total / cols)) -- Real per-icon flow packing: each slot's OWN actual render size (its fx Size -- override, or the bar's base iconSize) drives its own footprint directly, so @@ -5240,26 +5160,17 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) local rowWidth, rowHeight, colOffset, rowYOffset = {}, {}, {}, {} do - -- Weapon enchants lead row 0 at the bar's own corner, and the aura block - -- starts past them on EVERY row -- ShiftBuffsForEnchants moves the whole - -- container, not just its first line, so lower rows stay indented by the - -- same amount and the first row overflows the reserved grid by the - -- shift. That asymmetry is the real bar's behavior; packing enchants as - -- plain leading members of one uniform flow would wrap row 2 back to the - -- bar's edge and misrepresent it. - local enchShift = 0 - for k = 1, numEnch do - colOffset[k] = enchShift - enchShift = enchShift + slotSize[k] + pad - rowHeight[0] = math.max(rowHeight[0] or 0, slotSize[k]) - end - + -- ONE uniform flow, enchants first: the engine lays the weapon-enchant + -- frames out as their own layout group placed BEFORE the aura groups + -- on the same container, so they lead row 0 as plain members of the + -- line and row 2 wraps back to the bar's own edge -- no per-row + -- indent, and the reserved grid is never overflowed. local runningX, runningY = {}, 0 - for r = 0, rows - 1 do runningX[r] = enchShift end - for i = numEnch + 1, total do - local r = math.floor((i - numEnch - 1) / cols) - colOffset[i] = runningX[r] - runningX[r] = runningX[r] + slotSize[i] + pad + for r = 0, rows - 1 do runningX[r] = 0 end + for i = 1, total do + local r = math.floor((i - 1) / cols) + colOffset[i] = runningX[r] or 0 + runningX[r] = (runningX[r] or 0) + slotSize[i] + pad rowHeight[r] = math.max(rowHeight[r] or 0, slotSize[i]) end for r = 0, rows - 1 do @@ -5292,10 +5203,7 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) icons[i] = btn end - -- Enchant cells all live on row 0; aura slots index into their own - -- block, which starts after them (see the packing block above). - local row = (i <= numEnch) and 0 - or math.floor((i - numEnch - 1) / cols) + local row = math.floor((i - 1) / cols) local withinLineStep = colOffset[i] local acrossLinesStep = rowYOffset[row] -- btn's own anchor point is `corner` (matching growDirection/ @@ -5755,8 +5663,6 @@ function ns.PAB_SetEnabled(v) if debuffsParent then debuffsParent:Hide() end for _, parent in pairs(customBuffParents) do parent:Hide() end for _, parent in pairs(customDebuffParents) do parent:Hide() end - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end -- The re-hide hooks release once the master is off: hand Blizzard's -- native display back live. ShowBlizzardPlayerAuras() @@ -5804,8 +5710,6 @@ function ns.PAB_ApplyUseBlizzard() if s.useBlizzardBuffs == true then ApplyDefaultBarShown(true) ApplyDefaultBarShown(false) - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end ShowBlizzardPlayerAuras() RegisterPABUnlock() else diff --git a/EllesmereUI_AuraKit.lua b/EllesmereUI_AuraKit.lua index a783d992b..6871404a5 100644 --- a/EllesmereUI_AuraKit.lua +++ b/EllesmereUI_AuraKit.lua @@ -1362,6 +1362,64 @@ function AK.AddSlotToContainer(container, s) return f end +------------------------------------------------------------------------------ +-- Item enchantments (temporary weapon enchants: oils, imbues, stones) +-- +-- Not auras (an enchantID, no spell ID) but engine display sources since +-- 12.1: the container builds the frame through the SAME initializer as an +-- aura group, resolves the icon off the inventory slot, shows the item +-- tooltip, and runs the protected CancelTemporaryEnchantment on a cancel +-- click (style.cancelButtons feeds SetCancelAuraButtons, as for auras). +AK.ITEM_ENCH_SLOTS = { "MainHand", "OffHand", "Ranged" } + +-- Their position is LAYOUT data: they are a flow group of their own, and +-- `placement`/`layoutIndex` decides whether they lead or trail the aura +-- groups. Nothing may be anchored to a container (forbidden aspects). +function AK.SetContainerItemEnchantmentLayout(container, layout) + if not (container and layout and container.SetItemEnchantmentLayout) then return end + container:SetItemEnchantmentLayout(layout) +end + +function AK.SetContainerItemEnchantmentSort(container, sortMethod, sortDirection) + if not (container and sortMethod and container.SetItemEnchantmentSortMethod) then return end + container:SetItemEnchantmentSortMethod(sortMethod, sortDirection) +end + +-- e = { style, extraInit, hidePermanent (default true: duration-bearing +-- only), slots (default all three), layout, sortMethod, sortDirection }. +-- ONE-WAY: with no addon-facing unregister, a consumer that must stop +-- showing them releases the container and builds a fresh one. Re-calling is +-- idempotent and re-applies layout and sort only. +function AK.AddItemEnchantmentsToContainer(container, e) + if not (container and e and container.AddItemEnchantment) then return end + local slotEnum = AuraContainerItemEnchantmentSlot + if not slotEnum then return end + local cd = containerData[container] + if cd and not cd.itemEnchFrames then cd.itemEnchFrames = {} end + local tracked = cd and cd.itemEnchFrames + local names = e.slots or AK.ITEM_ENCH_SLOTS + for i = 1, #names do + local name = names[i] + local slot = slotEnum[name] + -- HasItemEnchantment is engine-private and a second declaration for + -- the same slot asserts, so the declared set is ours to keep. + if slot ~= nil and not (tracked and tracked[name]) then + local frame = container:AddItemEnchantment(slot, { + initializeFrame = AK.MakeInitializer(e.style, e.extraInit), + hidePermanent = e.hidePermanent ~= false, + }) + if tracked then tracked[name] = frame or true end + end + end + AK.SetContainerItemEnchantmentLayout(container, e.layout) + AK.SetContainerItemEnchantmentSort(container, e.sortMethod, e.sortDirection) +end + +function AK.HasItemEnchantments(container) + local cd = container and containerData[container] + return (cd and cd.itemEnchFrames and next(cd.itemEnchFrames) ~= nil) or false +end + -- Unit LAST: unit assignment re-evaluates event registrations, and those -- are gated on the container having groups/slots. Setting the unit before -- declaring content leaves UNIT_AURA unregistered (the Blizzard reference @@ -1545,17 +1603,23 @@ end function AK.ReleaseContainer(container) if not container then return end local data = containerData[container] - if data and data.slotFrames then - for _, slotButton in pairs(data.slotFrames) do - local d = bd[slotButton] - if d then - if d.styleKey and styleButtons[d.styleKey] then - styleButtons[d.styleKey][slotButton] = nil - end - bd[slotButton] = nil + local function Untrack(button) + local d = type(button) == "table" and bd[button] + if d then + if d.styleKey and styleButtons[d.styleKey] then + styleButtons[d.styleKey][button] = nil end + bd[button] = nil end end + if data and data.slotFrames then + for _, slotButton in pairs(data.slotFrames) do Untrack(slotButton) end + end + -- Item enchantment frames come back as handles on declaration, so unlike + -- group buttons they can be untracked instead of left as restyle zombies. + if data and data.itemEnchFrames then + for _, enchButton in pairs(data.itemEnchFrames) do Untrack(enchButton) end + end containerData[container] = nil container:Hide() end From 701f2874a03c385769bf3dddb467167f97ec23f0 Mon Sep 17 00:00:00 2001 From: JuJuFX-dev Date: Mon, 14 Sep 2026 19:27:00 +0200 Subject: [PATCH 2/5] fix(player-aurabars): uniform weapon-enchant seam and a stable bar box The gap between the weapon-enchant cells and the first buff was twice the gap between two buffs. Blizzard's flow layout trails EVERY element with elementSpacing, the last one of a group included, and groupSpacing is added on top of that, so a group boundary carried both. Both consumers hold groupSpacing at 0 now, which also halves the seam between the catch-all and the spells group. The cells share the line with the auras, so they come out of the grid the three sliders describe instead of being added on top: the aura cap drops by the declared enchant slots while the row is on, and the bar's box is identical with the row on or off. Reserving an extra row instead made the bar jump when the row was toggled. A grid smaller than three cells declares only as many slots as it can hold (main hand first, new maxSlots option), and that count rides the buffs content signature so a shrunk grid rebuilds the container. Enchants-only bars keep their full cap, their grid was auto-sized for exactly these cells. Checked against Blizzard's own AnchorUtil.ApplyFlowLayout rather than by eye: 17280 grid configurations render inside their box, 2160 toggle pairs leave the geometry untouched, 90 spacing cases keep every gap uniform including the seam. Also from an independent review of the migration: ApplyEnchants no longer runs on the container ApplyLiveConfig is about to retire (three engine frames leaked per toggle), CreateBars retires the previous containers before replacing them (a second build left orphans that rendered every icon twice), PAB_ProfileResync reconciles the buffs content signature so a profile swap picks up a changed spell set or enchant row, AddItemEnchantment is pcall'd and the sort setter requires both halves because the engine validates before it defaults, the options preview draws the cells in the live order, and the Filters tooltip no longer describes the retired grid shift. --- .../EUI_PlayerAuraBars_ManagerPages.lua | 2 +- .../EUI_UnitFrames_AuraContainers.lua | 15 +- .../EllesmereUIUnitFrames_PlayerAuraBars.lua | 142 +++++++++++++----- EllesmereUI_AuraKit.lua | 27 ++-- 4 files changed, 123 insertions(+), 63 deletions(-) diff --git a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua index 441fe5604..fef42fc31 100644 --- a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua +++ b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua @@ -570,7 +570,7 @@ local function BuildAssignedBuffsFields(frame, fontPath, sy, cfg, apply, isDefau -- alone, so the row would be a dead switch on custom buff bars. if isDefault then items[#items + 1] = { key = PAB_WEAPON_ENCH_KEY, label = "Weapon Enchants", - tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above -- and the aura grid is shifted inward to make room for them, with every row shifting over by the same amount." } + tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above -- and they take the first cells of the bar, pushing the buffs along the row." } end items[#items + 1] = { isHeader = true, label = "Show", rightLabel = "Hide" } for i = 1, #filters do diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua index 0b3b43074..6ec43a9f6 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua @@ -1333,7 +1333,7 @@ local function AnchorContainer(container, frame, unit, base, s, buffContainer) elementHeight = h, elementSpacing = gap, lineSpacing = lineGap, - groupSpacing = gap, + -- No groupSpacing, see the aura groups' layout below. groupLineSpacing = lineGap, placement = placement and placement.BeforeAuraGroups, }, @@ -1424,14 +1424,11 @@ local function ApplyGroupConfig(container, unit, base, s, chain, declared) cand.excludeSpellIDs = ex end - -- groupSpacing is the gap BEFORE a group that continues the line, so the - -- distance from the leading weapon-enchant group to the first aura has to - -- sit on the AURA groups. Only where enchants are actually declared (the - -- player's buff container): every other element keeps its existing - -- zero inter-group spacing. - local groupGap = AK.HasItemEnchantments(container) and spX or nil - local layout = { elementWidth = size, elementHeight = h, elementSpacing = spX, lineSpacing = spY, - groupSpacing = groupGap } + -- No groupSpacing: the engine trails every element with elementSpacing, + -- the last one of a group included, so a group boundary -- the seam + -- between the weapon-enchant cells and the first aura -- already sits one + -- spX away, and groupSpacing would double it. + local layout = { elementWidth = size, elementHeight = h, elementSpacing = spX, lineSpacing = spY } -- Active set = "all" (a non-player BUFF element with no classes enabled) -- or each chain link. Every other declared group -- disabled classes and diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua index 7320ece44..3aa0e4137 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua @@ -689,9 +689,11 @@ local STYLE_DEBUFFS = "playerAuraBars_debuffs" -- borderTexture ("solid" or a built-in/LibSharedMedia key), optional -- borderTextureOffset/OffsetY/ShiftX/ShiftY, and borderBehind; -- padding (single scalar -> all 4 sides); rowSpacing (optional row gap: feeds --- lineSpacing/groupLineSpacing only, nil falls back to `padding`; elementSpacing/ --- groupSpacing, icon-to-icon within a row, always stay tied to `padding`); maxTotal --- (overall icon cap); iconsPerRow (row width in columns); maxRows (row cap; with +-- lineSpacing/groupLineSpacing only, nil falls back to `padding`; elementSpacing, +-- icon-to-icon within a row AND across a group seam, stays tied to `padding` +-- while groupSpacing is held at 0, see BuildGroupLayout); maxTotal +-- (overall icon cap, weapon-enchant cells included while that row is on); +-- iconsPerRow (row width in columns); maxRows (row cap; with -- iconsPerRow also bounds maxTotal, see ComputeGrid()); growDirection ("LEFT"/"RIGHT"/ -- "CENTER_HORIZONTAL"/"CENTER_VERTICAL"/"UP"/"DOWN", default LEFT). -- Buff bars (default AND custom, one model): filters ([filterId]=true, shared PAB @@ -1291,7 +1293,13 @@ local function BuildGroupLayout(cfg, gap, rowGap, size) elementHeight = size, elementSpacing = gap, lineSpacing = rowGap, - groupSpacing = gap, + -- ZERO on purpose: the engine's flow layout advances the cursor by + -- elementSpacing AFTER every element, the last one of a group + -- included, so a group boundary already carries one `gap`. Adding + -- groupSpacing on top doubled it -- visible as a wider seam between + -- the weapon-enchant cells and the first buff, and between the + -- catch-all and spells groups. + groupSpacing = 0, groupLineSpacing = rowGap, } end @@ -1299,13 +1307,13 @@ end local function ApplyGroupConfig(container, chain, declaredSet, styleKey, effectiveMax, gap, rowGap, cfg, extraCand) local sortMethod = ResolveSortMethod(cfg) local sortDirection = ResolveSortDirection(cfg) - -- elementSpacing = icon-to-icon gap in a row; lineSpacing = gap between wrapped - -- rows within a group; group*Spacing = gap to the NEXT group on the same - -- container. elementSpacing/groupSpacing stay tied to `gap` (padding); - -- lineSpacing/groupLineSpacing use `rowGap` (defaults to `gap`) so row-to-row - -- distance is overridable independently of icon-to-icon spacing (cfg.rowSpacing in - -- the Settings Schema comment). Container-level padding is a THIRD, unrelated - -- concept: the OUTER edge inset, fixed at 0 elsewhere and never affected by either. + -- elementSpacing = icon-to-icon gap in a row, and it also separates two + -- groups on one line (see BuildGroupLayout's zero groupSpacing); + -- lineSpacing = gap between wrapped rows within a group, tied to `rowGap` + -- (defaults to `gap`) so row-to-row distance is overridable independently of + -- icon-to-icon spacing (cfg.rowSpacing in the Settings Schema comment). + -- Container-level padding is a THIRD, unrelated concept: the OUTER edge inset, + -- fixed at 0 elsewhere and never affected by either. local active = {} for i = 1, #chain do local link = chain[i] @@ -1457,6 +1465,12 @@ local function MaxIconSizeFor(isBuff, cfg) return PabSnap(shaped), shaped end +-- Engine-declared weapon-enchant slots (AuraContainerItemEnchantmentSlot: +-- main hand, off hand, ranged). Only the first two are reachable in current +-- retail content, but the engine declares all three, so anything reserving +-- space for them must tolerate three. +local ENCH_SLOT_COUNT = 3 + local function ComputeGrid(isBuff, cfg) local iconSize, rawIconSize = MaxIconSizeFor(isBuff, cfg) local pad = cfg.padding or 5 @@ -1469,6 +1483,22 @@ local function ComputeGrid(isBuff, cfg) local effectiveMax = math.min(configuredMax, rows * cols) -- Actual rows needed for the effective cap, never more than the row limit local usedRows = math.min(rows, math.max(1, math.ceil(effectiveMax / cols))) + -- Weapon enchants share the line with the auras (their own leading layout + -- group, see BuildEnchantSpec), so their cells come OUT of the grid the + -- three sliders describe: the aura cap drops by the declared slots while + -- the row is on, and the box geometry stays identical whether the row is on + -- or off. Growing the box instead made toggling the row jump the bar by a + -- row (field report). A grid under three cells declares only what it can + -- hold, main hand first, so the cells can never spill out. + local auraMax, enchSlots = effectiveMax, 0 + if isBuff and cfg.showWeaponEnchants == true then + enchSlots = math.min(ENCH_SLOT_COUNT, effectiveMax) + -- Enchants-only bars keep the full cap: their grid was auto-sized FOR + -- these cells (SyncWeaponEnchantsGrid) and no aura group holds content. + if not (ns.PAB_IsWeaponEnchantsOnly and ns.PAB_IsWeaponEnchantsOnly(cfg)) then + auraMax = effectiveMax - enchSlots + end + end -- `lineExtent` is the icons' own extent on the line axis (iconsPerRow -- icons of iconSize + gaps); `crossExtent` is the other axis (lines actually used). -- Horizontal growth: a "line" is a row, so lineExtent -> width. Vertical growth @@ -1498,7 +1528,9 @@ local function ComputeGrid(isBuff, cfg) local legacyLine = cols * legacyIcon + (cols - 1) * legacyPad local legacyCross = usedRows * legacyIcon + (usedRows - 1) * legacyRowGap return { - effectiveMax = effectiveMax, + -- Aura cap, already net of the weapon-enchant cells (see auraMax). + effectiveMax = auraMax, + enchSlots = enchSlots, rowWidth = rowWidth, width = width, height = height, @@ -2335,7 +2367,7 @@ end -- grow direction and in combat. Opt-in per bar (showWeaponEnchants, the -- pinned "Weapon Enchants" Filters row), a content source of its own and so -- never gated on the broad-content modes. -local function BuildEnchantSpec(cfg, pad, rowGap) +local function BuildEnchantSpec(cfg, pad, rowGap, maxSlots) local layout = BuildGroupLayout(cfg, pad, rowGap) local placement = CustomAuraContainerItemEnchantmentPlacement if placement then layout.placement = placement.BeforeAuraGroups end @@ -2345,6 +2377,7 @@ local function BuildEnchantSpec(cfg, pad, rowGap) style = STYLE_BUFFS, layout = layout, hidePermanent = true, + maxSlots = maxSlots, -- REVERSE keeps MAIN HAND adjacent to the aura run: the engine puts -- the group's first element at the leading edge, and Slot order is -- main hand, off hand, ranged. @@ -2357,17 +2390,21 @@ end -- a live padding/icon-size change follows. Turning the row OFF is served by -- the content signature below instead: the engine has no addon-facing -- unregister, so the container has to be rebuilt for that. -local function ApplyEnchants(container, cfg, pad, rowGap) - if not (container and cfg and cfg.showWeaponEnchants == true) then return end - AK.AddItemEnchantmentsToContainer(container, BuildEnchantSpec(cfg, pad, rowGap)) +local function ApplyEnchants(container, cfg, pad, grid) + if not (container and cfg and grid and cfg.showWeaponEnchants == true) then return end + if (grid.enchSlots or 0) <= 0 then return end + AK.AddItemEnchantmentsToContainer(container, + BuildEnchantSpec(cfg, pad, grid.rowGap, grid.enchSlots)) end --- Buffs content signature: the resolved spell set PLUS the weapon-enchant --- row. A group's candidateFilters are fixed at declaration and an item --- enchantment cannot be undeclared at all, so a change in either releases --- the container and builds a fresh one. -local function BuffsContentSig(cfg, spells) - return table.concat(spells, ",") .. (cfg.showWeaponEnchants == true and "|e" or "") +-- Buffs content signature: the resolved spell set PLUS the number of declared +-- weapon-enchant slots. A group's candidateFilters are fixed at declaration +-- and an item enchantment cannot be undeclared at all, so a change in either +-- releases the container and builds a fresh one -- including a grid shrunk +-- below three cells, which declares fewer slots than before. +local function BuffsContentSig(cfg, spells, enchSlots) + enchSlots = (cfg.showWeaponEnchants == true) and (enchSlots or 0) or 0 + return table.concat(spells, ",") .. (enchSlots > 0 and ("|e" .. enchSlots) or "") end local function CreateBars() @@ -2495,11 +2532,18 @@ local function CreateBars() -- the sig-diffing. local buffAllChain = BuffBarChain(buffCfg) local buffSpells = ns.PAB_ResolveSpells(buffCfg) - buffsSlotSig = BuffsContentSig(buffCfg, buffSpells) + buffsSlotSig = BuffsContentSig(buffCfg, buffSpells, buffGrid.enchSlots) + -- This runs more than once per session (master re-enable, Use Blizzard + -- Buffs off, a profile swap whose content signature moved), and a container + -- can never be destroyed: retire the previous pair first or the orphans stay + -- shown on the same parent and every aura -- and every enchant cell -- renders + -- twice. + RetireContainer(buffsContainer, declared.buffs) + RetireContainer(debuffsContainer, declared.debuffs) AK.RequestContainer(buffsParent, "player", buffSpec, function(container) buffsContainer = container ApplyContainerAnchorAndGrowth(container, buffsParent, buffCfg, buffGrid) - ApplyEnchants(container, buffCfg, buffPad, buffGrid.rowGap) + ApplyEnchants(container, buffCfg, buffPad, buffGrid) declared.buffs = {} ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, buffGrid.effectiveMax, buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) if #buffSpells > 0 then @@ -2852,11 +2896,6 @@ local function ApplyLiveConfig(isBuff) ApplyContainerAnchorAndGrowth(container, parent, cfg, grid) if isBuff then - -- Re-applies the enchant group's layout so a live padding/icon-size - -- change follows; the row going OFF rebuilds the container through - -- the signature below instead. - ApplyEnchants(container, cfg, pad, grid.rowGap) - -- Unlock mode bakes the mover's label at registration, so a Filters -- change would leave the old name on it until the next CreateBars. -- Re-register only when the name actually changed -- this function runs @@ -2869,7 +2908,7 @@ local function ApplyLiveConfig(isBuff) if isBuff then local spells = ns.PAB_ResolveSpells(cfg) - local sig = BuffsContentSig(cfg, spells) + local sig = BuffsContentSig(cfg, spells, grid.enchSlots) local allChain = BuffBarChain(cfg) if sig ~= buffsSlotSig then -- Safe to fully release+rebuild: the default Buffs container holds only the @@ -2884,7 +2923,7 @@ local function ApplyLiveConfig(isBuff) AK.RequestContainer(parent, "player", spec, function(newContainer) buffsContainer = newContainer ApplyContainerAnchorAndGrowth(newContainer, parent, cfg, grid) - ApplyEnchants(newContainer, cfg, pad, grid.rowGap) + ApplyEnchants(newContainer, cfg, pad, grid) declared.buffs = {} ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if #spells > 0 then @@ -2910,6 +2949,11 @@ local function ApplyLiveConfig(isBuff) -- zeroes the catch-all when `allChain` is empty, so one call covers on and -- off. The spells group isn't part of that chain path, so its -- maxFrameCount/layout/sort are refreshed here directly. + -- + -- The enchant layout rides this branch (not the pass above): on the + -- rebuild path the container here is the one about to be retired, + -- and declaring three engine frames on it would leak them. + ApplyEnchants(container, cfg, pad, grid) ApplyGroupConfig(container, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if declared.buffs.spells then container:SetAuraGroupMaxFrameCount("spells", grid.effectiveMax) @@ -4512,9 +4556,12 @@ local PREVIEW_ENCHANT_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17 } -- unknown-icon question mark there would advertise a cell the player can never -- fill. An unarmed character still gets the single main-hand cell, so ticking -- the option always previews as something rather than silently nothing. +-- Walked in REVERSE, mirroring the live group's sortDirection: the engine puts +-- the first element at the leading edge, so main hand ends up adjacent to the +-- aura run (see BuildEnchantSpec), off hand out at the corner. local function PreviewEnchantSlots() local out = {} - for i = 1, #PREVIEW_ENCHANT_SLOTS do + for i = #PREVIEW_ENCHANT_SLOTS, 1, -1 do local slot = PREVIEW_ENCHANT_SLOTS[i] if GetInventoryItemTexture("player", slot) then out[#out + 1] = slot end end @@ -4913,19 +4960,18 @@ local function BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- appear, not just their order. Truncate to `count` on the stable, sort- -- independent mixed order FIRST, then sort that fixed selection for display. -- Weapon enchants take the LEADING cells and are never sorted into the aura - -- content: they are not auras. They are also ADDITIVE, not a slice of the - -- bar's capacity -- the live container keeps its full maxFrameCount and the - -- engine flows the enchant group ahead of the aura groups, so an enchant - -- never costs an aura its slot (it does share the line, so the wrap moves). + -- content: they are not auras. `count` is already net of them (ComputeGrid + -- drops the aura cap by the three engine slots while the row is on), so the + -- rendered total still fits the bar's own grid. local numEnch, enchSlots = 0, nil if isBuff and cfg.showWeaponEnchants == true then enchSlots = PreviewEnchantSlots() numEnch = #enchSlots end -- The two modes are genuinely different shapes and the preview mirrors both: - -- * alongside auras -- the container keeps its full maxFrameCount and the - -- enchant group leads the flow, so they cost no aura its slot but do - -- share the line: a full row wraps that many icons further down. + -- * alongside auras -- the enchant group leads the flow and its cells are + -- already subtracted from the aura cap, so the bar's footprint is the + -- same with the row on or off. -- * enchants-only -- the grid was auto-sized FOR the enchants -- (SyncWeaponEnchantsGrid) and the container holds no groups, so the -- cells sit INSIDE that reserved width. The leftover cells stay as @@ -5163,8 +5209,8 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) -- ONE uniform flow, enchants first: the engine lays the weapon-enchant -- frames out as their own layout group placed BEFORE the aura groups -- on the same container, so they lead row 0 as plain members of the - -- line and row 2 wraps back to the bar's own edge -- no per-row - -- indent, and the reserved grid is never overflowed. + -- line and row 2 wraps back to the bar's own edge, no per-row indent. + -- Their cells come out of the aura cap, so the box always fits. local runningX, runningY = {}, 0 for r = 0, rows - 1 do runningX[r] = 0 end for i = 1, total do @@ -5694,6 +5740,20 @@ function ns.PAB_ProfileResync() CreateBars() return end + -- Content that can only change by rebuilding the container (the resolved + -- spell set, and the weapon-enchant row the engine cannot undeclare) does + -- not reach ApplyLiveConfig on a swap, so the new profile's signature is + -- reconciled here. CreateBars is the safe lane for it: it applies the new + -- profile's sizes and positions outright instead of running + -- ApplyLiveConfig's size-rebase against the OLD profile's lastSize. + if s and s.enabled == true and s.useBlizzardBuffs ~= true and buffsParent then + local buffCfg = DefaultBuffsCfg(s) + local buffGrid = ComputeGrid(true, buffCfg) + if BuffsContentSig(buffCfg, ns.PAB_ResolveSpells(buffCfg), buffGrid.enchSlots) ~= buffsSlotSig then + CreateBars() + return + end + end ApplyDefaultBarShown(true) ApplyDefaultBarShown(false) SyncNativeAuras() diff --git a/EllesmereUI_AuraKit.lua b/EllesmereUI_AuraKit.lua index 6871404a5..62b6d396a 100644 --- a/EllesmereUI_AuraKit.lua +++ b/EllesmereUI_AuraKit.lua @@ -1380,13 +1380,17 @@ function AK.SetContainerItemEnchantmentLayout(container, layout) container:SetItemEnchantmentLayout(layout) end +-- BOTH halves required: the inbound setter validates before it defaults, so a +-- method without a direction is a hard error engine-side. function AK.SetContainerItemEnchantmentSort(container, sortMethod, sortDirection) - if not (container and sortMethod and container.SetItemEnchantmentSortMethod) then return end + if not (container and sortMethod and sortDirection + and container.SetItemEnchantmentSortMethod) then return end container:SetItemEnchantmentSortMethod(sortMethod, sortDirection) end -- e = { style, extraInit, hidePermanent (default true: duration-bearing --- only), slots (default all three), layout, sortMethod, sortDirection }. +-- only), maxSlots (declare only the first N of ITEM_ENCH_SLOTS, for a +-- display too small to hold all three), layout, sortMethod, sortDirection }. -- ONE-WAY: with no addon-facing unregister, a consumer that must stop -- showing them releases the container and builds a fresh one. Re-calling is -- idempotent and re-applies layout and sort only. @@ -1397,29 +1401,28 @@ function AK.AddItemEnchantmentsToContainer(container, e) local cd = containerData[container] if cd and not cd.itemEnchFrames then cd.itemEnchFrames = {} end local tracked = cd and cd.itemEnchFrames - local names = e.slots or AK.ITEM_ENCH_SLOTS - for i = 1, #names do + local names = AK.ITEM_ENCH_SLOTS + local last = math.min(e.maxSlots or #names, #names) + for i = 1, last do local name = names[i] local slot = slotEnum[name] -- HasItemEnchantment is engine-private and a second declaration for - -- the same slot asserts, so the declared set is ours to keep. + -- the same slot asserts, so the declared set is ours to keep. pcall'd + -- anyway: ReleaseContainer drops the whole containerData entry, so a + -- container that is released and then re-declared (no consumer does + -- that today) would hit that assert with the tracking gone. if slot ~= nil and not (tracked and tracked[name]) then - local frame = container:AddItemEnchantment(slot, { + local ok, frame = pcall(container.AddItemEnchantment, container, slot, { initializeFrame = AK.MakeInitializer(e.style, e.extraInit), hidePermanent = e.hidePermanent ~= false, }) - if tracked then tracked[name] = frame or true end + if ok and tracked then tracked[name] = frame or true end end end AK.SetContainerItemEnchantmentLayout(container, e.layout) AK.SetContainerItemEnchantmentSort(container, e.sortMethod, e.sortDirection) end -function AK.HasItemEnchantments(container) - local cd = container and containerData[container] - return (cd and cd.itemEnchFrames and next(cd.itemEnchFrames) ~= nil) or false -end - -- Unit LAST: unit assignment re-evaluates event registrations, and those -- are gated on the container having groups/slots. Setting the unit before -- declaring content leaves UNIT_AURA unregistered (the Blizzard reference From 765e1eb30c2a932dfd496859667422f1680bcc4d Mon Sep 17 00:00:00 2001 From: JuJuFX-dev Date: Mon, 14 Sep 2026 22:00:28 +0200 Subject: [PATCH 3/5] fix(player-aurabars): count weapon enchant cells against Max Icons live Max Icons is the whole bar's budget, so only the cells actually showing take a slot from it: a bar set to 32 renders 32 icons whether no oil, one or two are up, and a slot without a temporary enchant reserves nothing. The previous pass subtracted all three engine slots statically, which cost three buffs even with nothing enchanted (32 icons over 3 rows of 11 showed 29, and 32 over a single row of 6 showed 4 with one oil up). The cap follows WEAPON_ENCHANT_CHANGED and WEAPON_SLOT_CHANGED, registered only while the row is on and guarded on the count, so a charge tick re-applies nothing. Reading the three inventory slots is equipment state, not aura data, so it stays legal in restricted combat. The declaration cap is unchanged: a grid under three cells still declares only as many slots as it can hold, so the cells can never spill out of the bar, and the box geometry stays identical with the row on or off. The options preview follows the same rule, drawing its enchant cells out of the same budget, and the Filters tooltip is cut back to what the row does now that nothing is shifted. --- .../EUI_PlayerAuraBars_ManagerPages.lua | 2 +- .../EllesmereUIUnitFrames_PlayerAuraBars.lua | 118 ++++++++++++------ 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua index fef42fc31..7f366f670 100644 --- a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua +++ b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua @@ -570,7 +570,7 @@ local function BuildAssignedBuffsFields(frame, fontPath, sy, cfg, apply, isDefau -- alone, so the row would be a dead switch on custom buff bars. if isDefault then items[#items + 1] = { key = PAB_WEAPON_ENCH_KEY, label = "Weapon Enchants", - tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above -- and they take the first cells of the bar, pushing the buffs along the row." } + tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above." } end items[#items + 1] = { isHeader = true, label = "Show", rightLabel = "Hide" } for i = 1, #filters do diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua index 3aa0e4137..0fccded67 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua @@ -1483,21 +1483,13 @@ local function ComputeGrid(isBuff, cfg) local effectiveMax = math.min(configuredMax, rows * cols) -- Actual rows needed for the effective cap, never more than the row limit local usedRows = math.min(rows, math.max(1, math.ceil(effectiveMax / cols))) - -- Weapon enchants share the line with the auras (their own leading layout - -- group, see BuildEnchantSpec), so their cells come OUT of the grid the - -- three sliders describe: the aura cap drops by the declared slots while - -- the row is on, and the box geometry stays identical whether the row is on - -- or off. Growing the box instead made toggling the row jump the bar by a - -- row (field report). A grid under three cells declares only what it can - -- hold, main hand first, so the cells can never spill out. - local auraMax, enchSlots = effectiveMax, 0 + -- How many weapon-enchant slots may be DECLARED on this bar. A grid under + -- three cells declares only what it can hold (main hand first), so the + -- cells can never spill out of it. This is not a budget: an inactive slot + -- reserves nothing, only a cell that actually shows costs one (BuffAuraMax). + local enchSlots = 0 if isBuff and cfg.showWeaponEnchants == true then enchSlots = math.min(ENCH_SLOT_COUNT, effectiveMax) - -- Enchants-only bars keep the full cap: their grid was auto-sized FOR - -- these cells (SyncWeaponEnchantsGrid) and no aura group holds content. - if not (ns.PAB_IsWeaponEnchantsOnly and ns.PAB_IsWeaponEnchantsOnly(cfg)) then - auraMax = effectiveMax - enchSlots - end end -- `lineExtent` is the icons' own extent on the line axis (iconsPerRow -- icons of iconSize + gaps); `crossExtent` is the other axis (lines actually used). @@ -1528,8 +1520,9 @@ local function ComputeGrid(isBuff, cfg) local legacyLine = cols * legacyIcon + (cols - 1) * legacyPad local legacyCross = usedRows * legacyIcon + (usedRows - 1) * legacyRowGap return { - -- Aura cap, already net of the weapon-enchant cells (see auraMax). - effectiveMax = auraMax, + -- The bar's WHOLE icon budget (Max Icons), weapon-enchant cells + -- included -- BuffAuraMax takes the showing ones off it. + effectiveMax = effectiveMax, enchSlots = enchSlots, rowWidth = rowWidth, width = width, @@ -2390,7 +2383,60 @@ end -- a live padding/icon-size change follows. Turning the row OFF is served by -- the content signature below instead: the engine has no addon-facing -- unregister, so the container has to be rebuilt for that. +-- Inventory slots behind AuraContainerItemEnchantmentSlot, in the same order. +local ENCH_INV_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } + +-- Weapon enchants showing right now. Only duration-bearing ones render +-- (hidePermanent in BuildEnchantSpec), so an empty or permanently enchanted +-- slot costs nothing. Equipment state, not aura data: these returns carry no +-- secret flags, in restricted combat either. +local function ActiveEnchantCount(slots) + local api = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo + if not api then return 0 end + local n = 0 + for i = 1, math.min(slots or 0, #ENCH_INV_SLOTS) do + local info = api(ENCH_INV_SLOTS[i]) + if info and info.hasExpirationTime then n = n + 1 end + end + return n +end + +-- Aura cap for the Buffs bar: "Max Icons" is the whole bar's budget, so the +-- cells actually showing come off it and the rendered total stays at the +-- configured number. Nothing is reserved for a slot that is not enchanted. +local function BuffAuraMax(grid) + local slots = grid.enchSlots or 0 + if slots <= 0 then return grid.effectiveMax end + return math.max(0, grid.effectiveMax - ActiveEnchantCount(slots)) +end + +-- The budget moves with the enchants, so an applied or expired oil re-applies +-- it. Registered ONLY while the row is on, and the count is change-guarded: +-- WEAPON_ENCHANT_CHANGED also fires for charge ticks, which leave the cell +-- count alone. +local enchEventFrame, lastEnchCount +local function SyncEnchantEvents(want) + if want then + if not enchEventFrame then + enchEventFrame = CreateFrame("Frame") + enchEventFrame:SetScript("OnEvent", function() + local n = ActiveEnchantCount(#ENCH_INV_SLOTS) + if n == lastEnchCount then return end + lastEnchCount = n + if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end + end) + end + lastEnchCount = ActiveEnchantCount(#ENCH_INV_SLOTS) + enchEventFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") + enchEventFrame:RegisterEvent("WEAPON_SLOT_CHANGED") + elseif enchEventFrame then + enchEventFrame:UnregisterAllEvents() + lastEnchCount = nil + end +end + local function ApplyEnchants(container, cfg, pad, grid) + SyncEnchantEvents(cfg ~= nil and cfg.showWeaponEnchants == true) if not (container and cfg and grid and cfg.showWeaponEnchants == true) then return end if (grid.enchSlots or 0) <= 0 then return end AK.AddItemEnchantmentsToContainer(container, @@ -2545,7 +2591,7 @@ local function CreateBars() ApplyContainerAnchorAndGrowth(container, buffsParent, buffCfg, buffGrid) ApplyEnchants(container, buffCfg, buffPad, buffGrid) declared.buffs = {} - ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, buffGrid.effectiveMax, buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) + ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(buffGrid), buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) if #buffSpells > 0 then local includeMap = {} for i = 1, #buffSpells do includeMap[buffSpells[i]] = true end @@ -2553,7 +2599,7 @@ local function CreateBars() key = "spells", filter = { "HELPFUL" }, style = STYLE_BUFFS, - maxFrameCount = buffGrid.effectiveMax, + maxFrameCount = BuffAuraMax(buffGrid), candidateFilters = MergeCandidateFilters({ includeSpellIDs = includeMap }, BuffCandidateExtras(buffCfg)), sortMethod = ResolveSortMethod(buffCfg), sortDirection = ResolveSortDirection(buffCfg), @@ -2824,6 +2870,7 @@ local function ApplyLiveConfig(isBuff) -- the bar's parent -- nothing extra to tear down here. ApplyDefaultBarShown(isBuff) if s.enabled ~= true or cfg.enabled == false or s.useBlizzardBuffs == true then + if isBuff then SyncEnchantEvents(false) end return end local grid = ComputeGrid(isBuff, cfg) @@ -2925,7 +2972,7 @@ local function ApplyLiveConfig(isBuff) ApplyContainerAnchorAndGrowth(newContainer, parent, cfg, grid) ApplyEnchants(newContainer, cfg, pad, grid) declared.buffs = {} - ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) + ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(grid), pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if #spells > 0 then local includeMap = {} for i = 1, #spells do includeMap[spells[i]] = true end @@ -2933,7 +2980,7 @@ local function ApplyLiveConfig(isBuff) key = "spells", filter = { "HELPFUL" }, style = STYLE_BUFFS, - maxFrameCount = grid.effectiveMax, + maxFrameCount = BuffAuraMax(grid), candidateFilters = MergeCandidateFilters({ includeSpellIDs = includeMap }, BuffCandidateExtras(cfg)), sortMethod = ResolveSortMethod(cfg), sortDirection = ResolveSortDirection(cfg), @@ -2954,9 +3001,9 @@ local function ApplyLiveConfig(isBuff) -- rebuild path the container here is the one about to be retired, -- and declaring three engine frames on it would leak them. ApplyEnchants(container, cfg, pad, grid) - ApplyGroupConfig(container, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) + ApplyGroupConfig(container, allChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(grid), pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if declared.buffs.spells then - container:SetAuraGroupMaxFrameCount("spells", grid.effectiveMax) + container:SetAuraGroupMaxFrameCount("spells", BuffAuraMax(grid)) container:SetAuraGroupLayout("spells", BuildGroupLayout(cfg, pad, grid.rowGap)) local liveIncludeMap = {} for i = 1, #spells do liveIncludeMap[spells[i]] = true end @@ -4960,27 +5007,22 @@ local function BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- appear, not just their order. Truncate to `count` on the stable, sort- -- independent mixed order FIRST, then sort that fixed selection for display. -- Weapon enchants take the LEADING cells and are never sorted into the aura - -- content: they are not auras. `count` is already net of them (ComputeGrid - -- drops the aura cap by the three engine slots while the row is on), so the - -- rendered total still fits the bar's own grid. + -- content: they are not auras. They do take a cell each, though: Max Icons + -- counts the whole bar, so the aura slots below are what is left after them. local numEnch, enchSlots = 0, nil if isBuff and cfg.showWeaponEnchants == true then enchSlots = PreviewEnchantSlots() numEnch = #enchSlots end - -- The two modes are genuinely different shapes and the preview mirrors both: - -- * alongside auras -- the enchant group leads the flow and its cells are - -- already subtracted from the aura cap, so the bar's footprint is the - -- same with the row on or off. - -- * enchants-only -- the grid was auto-sized FOR the enchants - -- (SyncWeaponEnchantsGrid) and the container holds no groups, so the - -- cells sit INSIDE that reserved width. The leftover cells stay as - -- placeholders on purpose: they are what explains where the bar's width - -- comes from, and dropping them made the 3-wide frame look arbitrary. - local avail = count - if isBuff and ns.PAB_IsWeaponEnchantsOnly(cfg) then - avail = math.max(0, count - numEnch) - end + -- Both modes render exactly `count` cells, which is what Max Icons promises: + -- the enchant cells lead, the aura slots take the rest. The enchants-only + -- bar keeps its leftovers as placeholders on purpose -- they are what + -- explains where the bar's width comes from. + -- + -- The live bar counts the enchants ACTUALLY up (BuffAuraMax); the preview + -- counts the weapon slots that could carry one, so an unenchanted character + -- still sees the shape the option produces. + local avail = math.max(0, count - numEnch) local mixed = isBuff and DedupeByIcon(BuildMixedRealSpells(cfg)) or nil local extraIDs @@ -5709,6 +5751,7 @@ function ns.PAB_SetEnabled(v) if debuffsParent then debuffsParent:Hide() end for _, parent in pairs(customBuffParents) do parent:Hide() end for _, parent in pairs(customDebuffParents) do parent:Hide() end + SyncEnchantEvents(false) -- The re-hide hooks release once the master is off: hand Blizzard's -- native display back live. ShowBlizzardPlayerAuras() @@ -5770,6 +5813,7 @@ function ns.PAB_ApplyUseBlizzard() if s.useBlizzardBuffs == true then ApplyDefaultBarShown(true) ApplyDefaultBarShown(false) + SyncEnchantEvents(false) ShowBlizzardPlayerAuras() RegisterPABUnlock() else From 46de40bf8c9677dc30081cd891a9f8a40fe3f740 Mon Sep 17 00:00:00 2001 From: JuJuFX-dev Date: Thu, 17 Sep 2026 11:23:46 +0200 Subject: [PATCH 4/5] fix(player-aurabars): keep the enchant helpers under the 200-locals limit Rebased onto upstream/main, whose new chunk-level locals pushed the file past Lua 5.1's 200-locals-per-chunk limit together with the weapon-enchant helpers. The count and event helpers now live in a do...end block; only BuffAuraMax and SyncEnchantEvents stay chunk-level. --- .../EllesmereUIUnitFrames_PlayerAuraBars.lua | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua index 0fccded67..bb264fa66 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua @@ -2383,55 +2383,60 @@ end -- a live padding/icon-size change follows. Turning the row OFF is served by -- the content signature below instead: the engine has no addon-facing -- unregister, so the container has to be rebuilt for that. --- Inventory slots behind AuraContainerItemEnchantmentSlot, in the same order. -local ENCH_INV_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } - --- Weapon enchants showing right now. Only duration-bearing ones render --- (hidePermanent in BuildEnchantSpec), so an empty or permanently enchanted --- slot costs nothing. Equipment state, not aura data: these returns carry no --- secret flags, in restricted combat either. -local function ActiveEnchantCount(slots) - local api = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo - if not api then return 0 end - local n = 0 - for i = 1, math.min(slots or 0, #ENCH_INV_SLOTS) do - local info = api(ENCH_INV_SLOTS[i]) - if info and info.hasExpirationTime then n = n + 1 end - end - return n -end - --- Aura cap for the Buffs bar: "Max Icons" is the whole bar's budget, so the --- cells actually showing come off it and the rendered total stays at the --- configured number. Nothing is reserved for a slot that is not enchanted. -local function BuffAuraMax(grid) - local slots = grid.enchSlots or 0 - if slots <= 0 then return grid.effectiveMax end - return math.max(0, grid.effectiveMax - ActiveEnchantCount(slots)) -end - --- The budget moves with the enchants, so an applied or expired oil re-applies --- it. Registered ONLY while the row is on, and the count is change-guarded: --- WEAPON_ENCHANT_CHANGED also fires for charge ticks, which leave the cell --- count alone. -local enchEventFrame, lastEnchCount -local function SyncEnchantEvents(want) - if want then - if not enchEventFrame then - enchEventFrame = CreateFrame("Frame") - enchEventFrame:SetScript("OnEvent", function() - local n = ActiveEnchantCount(#ENCH_INV_SLOTS) - if n == lastEnchCount then return end - lastEnchCount = n - if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end - end) +-- Wrapped in do...end: this file sits at Lua 5.1's 200-locals-per-chunk +-- limit, so only the two helpers used below stay chunk-level locals. +local BuffAuraMax, SyncEnchantEvents +do + -- Inventory slots behind AuraContainerItemEnchantmentSlot, in the same order. + local ENCH_INV_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } + + -- Weapon enchants showing right now. Only duration-bearing ones render + -- (hidePermanent in BuildEnchantSpec), so an empty or permanently enchanted + -- slot costs nothing. Equipment state, not aura data: these returns carry no + -- secret flags, in restricted combat either. + local function ActiveEnchantCount(slots) + local api = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo + if not api then return 0 end + local n = 0 + for i = 1, math.min(slots or 0, #ENCH_INV_SLOTS) do + local info = api(ENCH_INV_SLOTS[i]) + if info and info.hasExpirationTime then n = n + 1 end + end + return n + end + + -- Aura cap for the Buffs bar: "Max Icons" is the whole bar's budget, so the + -- cells actually showing come off it and the rendered total stays at the + -- configured number. Nothing is reserved for a slot that is not enchanted. + function BuffAuraMax(grid) + local slots = grid.enchSlots or 0 + if slots <= 0 then return grid.effectiveMax end + return math.max(0, grid.effectiveMax - ActiveEnchantCount(slots)) + end + + -- The budget moves with the enchants, so an applied or expired oil re-applies + -- it. Registered ONLY while the row is on, and the count is change-guarded: + -- WEAPON_ENCHANT_CHANGED also fires for charge ticks, which leave the cell + -- count alone. + local enchEventFrame, lastEnchCount + function SyncEnchantEvents(want) + if want then + if not enchEventFrame then + enchEventFrame = CreateFrame("Frame") + enchEventFrame:SetScript("OnEvent", function() + local n = ActiveEnchantCount(#ENCH_INV_SLOTS) + if n == lastEnchCount then return end + lastEnchCount = n + if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end + end) + end + lastEnchCount = ActiveEnchantCount(#ENCH_INV_SLOTS) + enchEventFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") + enchEventFrame:RegisterEvent("WEAPON_SLOT_CHANGED") + elseif enchEventFrame then + enchEventFrame:UnregisterAllEvents() + lastEnchCount = nil end - lastEnchCount = ActiveEnchantCount(#ENCH_INV_SLOTS) - enchEventFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") - enchEventFrame:RegisterEvent("WEAPON_SLOT_CHANGED") - elseif enchEventFrame then - enchEventFrame:UnregisterAllEvents() - lastEnchCount = nil end end From f24a8d9e4591174ff2d51e0385bc431474cf4292 Mon Sep 17 00:00:00 2001 From: JuJuFX-dev Date: Fri, 18 Sep 2026 07:28:40 +0200 Subject: [PATCH 5/5] locale: correct the _keys.txt key count to 800 upstream/main carries 800 keys in the body of EllesmereUILocales/_keys.txt but still says 791 in the header, left behind when two branches that both added keys were merged. The Locale keys up to date check regenerates the file and fails on that single line for every PR that touches Lua. Only the count changes; the key list is untouched. --- EllesmereUILocales/_keys.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EllesmereUILocales/_keys.txt b/EllesmereUILocales/_keys.txt index 67d60c51e..0e47324fe 100644 --- a/EllesmereUILocales/_keys.txt +++ b/EllesmereUILocales/_keys.txt @@ -1,6 +1,6 @@ # Auto-generated by .tools/extract-locale-keys.sh -- do not edit by hand. # Canonical list of translatable English keys passed as string literals -# (791 unique). Regenerate after wrapping new strings. Keys passed as +# (800 unique). Regenerate after wrapping new strings. Keys passed as # variables are not listed here -- use the in-game /euiloc harvester for # the complete runtime set. (Raid)