From 4143db140d179aa25aaa25de676345f07e7c67f9 Mon Sep 17 00:00:00 2001 From: Glyalith Date: Fri, 7 Aug 2026 20:42:36 -0600 Subject: [PATCH 1/2] Fix Combat Text Font never applying for SharedMedia fonts Bundled fonts store a direct file path and worked. A SharedMedia font stores "smf:" and resolved it at startup, which failed twice over: The earliest apply with SavedVariables available is our own ADDON_LOADED, and external SharedMedia packs load after us, so at that moment the font name is not registered yet. LSM:Fetch without its noDefault flag does not return nil for an unregistered name, it substitutes the DEFAULT font, Friz Quadrata. So the "skip if unavailable" guard never fired and DAMAGE_TEXT_FONT was set to the font damage numbers already use, right in the window where the engine caches it. The PLAYER_LOGIN re-apply resolved the real path but landed after the cache. Net effect: the dropdown showed the chosen font, the damage numbers never changed. Fix mirrors the Name Font pattern already in this codebase: cache the resolved path (fctFontPath) at selection time, when the pack is necessarily loaded because its font is in the dropdown. Startup applies the cached path in the early window, fetches with noDefault so a missing name can never silently become Friz, refreshes the cache once login completes, and clears it when the providing pack is gone so a later login never points the engine at a missing file. Profiles saved before this fix have no cache; their first login populates it and the second login shows the font. Also keep the ADDON_LOADED watch alive until Blizzard_CombatText actually loads: our own addon always fired first and unregistered it, so the load-on-demand CombatTextFont object was never restyled at its real load moment. --- EUI__General_Options.lua | 10 +++++++++ EllesmereUI_Startup.lua | 46 ++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index f86a6f18..1fc4e2f3 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -1860,8 +1860,18 @@ initFrame:SetScript("OnEvent", function(self) if not EllesmereUIDB then EllesmereUIDB = {} end if v == "default" then EllesmereUIDB.fctFont = nil + EllesmereUIDB.fctFontPath = nil else EllesmereUIDB.fctFont = v + -- Cache the resolved path (Name Font pattern). At next + -- login the startup apply runs at OUR ADDON_LOADED, before + -- an external SharedMedia pack has registered its fonts, + -- so an smf: key cannot be resolved that early -- but the + -- engine caches DAMAGE_TEXT_FONT in that window. Here the + -- pack is loaded (its font is in this dropdown), so the + -- path is known-good. + local e = fctFontValues[v] + EllesmereUIDB.fctFontPath = e and e.font or nil end EllesmereUI:ShowConfirmPopup({ title = "Logout Required", diff --git a/EllesmereUI_Startup.lua b/EllesmereUI_Startup.lua index 1ea8041a..e3af4116 100644 --- a/EllesmereUI_Startup.lua +++ b/EllesmereUI_Startup.lua @@ -304,19 +304,42 @@ end -- CombatTextFont may not exist yet here, so we also hook ADDON_LOADED -- to catch it as soon as it becomes available. do - local function ApplyCombatTextFont() - local saved = EllesmereUIDB and EllesmereUIDB.fctFont + local function ApplyCombatTextFont(loginComplete) + local db = EllesmereUIDB + local saved = db and db.fctFont if not saved or type(saved) ~= "string" or saved == "" then return end -- Resolve "smf:" prefixed SharedMedia font keys to actual paths local fontPath = saved local smName = saved:match("^smf:(.+)") if smName then local LSM = LibStub and LibStub("LibSharedMedia-3.0", true) - local fetched = LSM and LSM:Fetch("font", smName) - -- If the SM addon is missing or hasn't loaded yet, skip entirely - -- so Blizzard's default combat text font stays intact. - if not fetched then return end - fontPath = fetched + -- noDefault: an unregistered key must yield nil here. Without it + -- Fetch silently substitutes the DEFAULT font (Friz Quadrata), + -- which then lands in DAMAGE_TEXT_FONT right as the engine caches + -- it -- external SM packs load after us, so at our ADDON_LOADED + -- their fonts are never registered yet. + local fetched = LSM and LSM:Fetch("font", smName, true) + local cached = db.fctFontPath + if fetched then + db.fctFontPath = fetched -- keep the login cache current + fontPath = fetched + elseif loginComplete then + -- Every login addon has loaded and the font is still not + -- registered: the providing pack is gone. Drop the stale + -- cache so no later login points the engine at a missing + -- file, and leave Blizzard's default intact this session. + db.fctFontPath = nil + return + elseif type(cached) == "string" and cached ~= "" then + -- Early window (pack not loaded yet): use the path cached at + -- selection time. This is what makes an smf: font survive the + -- engine's login cache at all. + fontPath = cached + else + -- Pre-cache profile: nothing usable this early. The login- + -- complete pass above will populate the cache for next time. + return + end end _G.DAMAGE_TEXT_FONT = fontPath if _G.CombatTextFont then @@ -341,14 +364,19 @@ do end end - ApplyCombatTextFont() + ApplyCombatTextFont(event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD") ApplyUnitNameFont() if event == "PLAYER_LOGIN" then self:UnregisterEvent("PLAYER_LOGIN") elseif event == "PLAYER_ENTERING_WORLD" then self:UnregisterEvent("PLAYER_ENTERING_WORLD") - elseif event == "ADDON_LOADED" then + elseif addonName == "Blizzard_CombatText" then + -- Only Blizzard_CombatText's load retires the ADDON_LOADED watch. + -- Our own addon always loads first, so unregistering on the first + -- match (the old behavior) meant the CombatTextFont object -- the + -- load-on-demand scrolling-text font -- was never restyled at its + -- actual load moment. self:UnregisterEvent("ADDON_LOADED") end end) From c54a3b69e1a642babd2108f3d099f4d2adddba0e Mon Sep 17 00:00:00 2001 From: Glyalith Date: Fri, 7 Aug 2026 21:21:45 -0600 Subject: [PATCH 2/2] Harden the font path cache: probe before apply, no destructive clear Review findings on the first commit, worst first: with the providing pack uninstalled, the early window applied the cached path blindly, leaving DAMAGE_TEXT_FONT pointed at a missing file for the whole session, which disables floating combat text entirely. And the login-complete cache clear treated "not registered by PLAYER_LOGIN" as "pack gone", so a single login with the pack disabled (an alt, a troubleshooting session, out-of-date after a patch) or a pack that registers its fonts late wiped the account-wide cache and cost an extra default-font login after recovery. Replace the event-driven state machine with a probe: SetFont returns a success boolean, so both the LSM result and the cached path are verified loadable before either reaches an engine global or stays in the cache. That single invariant covers every reviewed scenario: an uninstalled pack clears the cache and leaves the default intact, a disabled-but-on-disk pack keeps working from the cache (font files load regardless of addon enable state), late-registering packs lose nothing, and a registration pointing at a moved file is refused. The loginComplete parameter goes away entirely. The cache also records which key it was resolved for (fctFontPathFor), so a fctFont written by any future non-dropdown path cannot silently apply a stale path for a different font, and it is only written for smf: keys, where the key is not already the path. ApplyUnitNameFont gets the same probe on its cached path: unitNameFontPath rides profile export, so an import from someone with a pack the importer lacks pointed UNIT_NAME_FONT at a missing file permanently. It now falls back to ResolveFontName and skips the write rather than apply a dead path. The ADDON_LOADED watch retires at our own load when no combat text font is configured (a mid-session pick needs a relog regardless) or when Blizzard_CombatText already loaded, so only enabled-feature sessions that still await that addon keep listening. --- EUI__General_Options.lua | 15 +++----- EllesmereUI_Startup.lua | 80 +++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index 1fc4e2f3..f173e917 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -1861,17 +1861,14 @@ initFrame:SetScript("OnEvent", function(self) if v == "default" then EllesmereUIDB.fctFont = nil EllesmereUIDB.fctFontPath = nil + EllesmereUIDB.fctFontPathFor = nil else EllesmereUIDB.fctFont = v - -- Cache the resolved path (Name Font pattern). At next - -- login the startup apply runs at OUR ADDON_LOADED, before - -- an external SharedMedia pack has registered its fonts, - -- so an smf: key cannot be resolved that early -- but the - -- engine caches DAMAGE_TEXT_FONT in that window. Here the - -- pack is loaded (its font is in this dropdown), so the - -- path is known-good. - local e = fctFontValues[v] - EllesmereUIDB.fctFontPath = e and e.font or nil + -- smf: keys cache their resolved path for the next login's + -- early window; see ApplyCombatTextFont in Startup. + local e = v:match("^smf:") and fctFontValues[v] + EllesmereUIDB.fctFontPath = e and e.font + EllesmereUIDB.fctFontPathFor = e and v end EllesmereUI:ShowConfirmPopup({ title = "Logout Required", diff --git a/EllesmereUI_Startup.lua b/EllesmereUI_Startup.lua index e3af4116..4aa9a41b 100644 --- a/EllesmereUI_Startup.lua +++ b/EllesmereUI_Startup.lua @@ -216,16 +216,30 @@ end -- Reads EllesmereUIDB.fonts directly rather than going through GetFontsDB(): -- that helper lazy-creates the table, and this can run at the ADDON_LOADED of -- Blizzard_CombatText, before our SavedVariables have been restored. + +-- Probe a font path before it reaches an engine global: SetFont returns a +-- success boolean, so a cached path whose providing addon was uninstalled is +-- detected instead of leaving the engine pointed at a missing file for the +-- whole session (which kills floating text entirely). +local probeFS +local function ProbeFont(path) + if type(path) ~= "string" or path == "" then return false end + probeFS = probeFS or UIParent:CreateFontString() + local ok, success = pcall(probeFS.SetFont, probeFS, path, 12, "") + return ok and success == true +end + local function ApplyUnitNameFont() local fonts = EllesmereUIDB and EllesmereUIDB.fonts local name = fonts and fonts.unitNameFont if not name or name == "" then return end local path = fonts.unitNameFontPath - if (type(path) ~= "string" or path == "") - and EllesmereUI and EllesmereUI.ResolveFontName then + local ok = ProbeFont(path) + if not ok and EllesmereUI and EllesmereUI.ResolveFontName then path = EllesmereUI.ResolveFontName(name) + ok = ProbeFont(path) end - if type(path) == "string" and path ~= "" then + if ok then _G.UNIT_NAME_FONT = path end end @@ -304,42 +318,30 @@ end -- CombatTextFont may not exist yet here, so we also hook ADDON_LOADED -- to catch it as soon as it becomes available. do - local function ApplyCombatTextFont(loginComplete) + -- smf: keys resolve via LSM when the providing pack has loaded, else via + -- the path cached at selection time (external packs load after us, so at + -- our ADDON_LOADED -- the window where the engine caches the global -- + -- the name is never registered yet). noDefault on Fetch: without it an + -- unregistered name silently resolves to the DEFAULT font, not nil. + -- Every candidate is probed, so only a loadable path is ever applied or + -- kept in the cache. + local function ApplyCombatTextFont() local db = EllesmereUIDB local saved = db and db.fctFont if not saved or type(saved) ~= "string" or saved == "" then return end - -- Resolve "smf:" prefixed SharedMedia font keys to actual paths local fontPath = saved local smName = saved:match("^smf:(.+)") if smName then local LSM = LibStub and LibStub("LibSharedMedia-3.0", true) - -- noDefault: an unregistered key must yield nil here. Without it - -- Fetch silently substitutes the DEFAULT font (Friz Quadrata), - -- which then lands in DAMAGE_TEXT_FONT right as the engine caches - -- it -- external SM packs load after us, so at our ADDON_LOADED - -- their fonts are never registered yet. local fetched = LSM and LSM:Fetch("font", smName, true) - local cached = db.fctFontPath - if fetched then - db.fctFontPath = fetched -- keep the login cache current - fontPath = fetched - elseif loginComplete then - -- Every login addon has loaded and the font is still not - -- registered: the providing pack is gone. Drop the stale - -- cache so no later login points the engine at a missing - -- file, and leave Blizzard's default intact this session. - db.fctFontPath = nil - return - elseif type(cached) == "string" and cached ~= "" then - -- Early window (pack not loaded yet): use the path cached at - -- selection time. This is what makes an smf: font survive the - -- engine's login cache at all. - fontPath = cached - else - -- Pre-cache profile: nothing usable this early. The login- - -- complete pass above will populate the cache for next time. - return - end + -- The cache only counts if it was written for the currently + -- saved key (fctFontPathFor pairs them). + local cached = (db.fctFontPathFor == saved) and db.fctFontPath or nil + fontPath = (ProbeFont(fetched) and fetched) + or (ProbeFont(cached) and cached) or nil + db.fctFontPath = fontPath + db.fctFontPathFor = fontPath and saved or nil + if not fontPath then return end end _G.DAMAGE_TEXT_FONT = fontPath if _G.CombatTextFont then @@ -364,19 +366,21 @@ do end end - ApplyCombatTextFont(event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD") + ApplyCombatTextFont() ApplyUnitNameFont() if event == "PLAYER_LOGIN" then self:UnregisterEvent("PLAYER_LOGIN") elseif event == "PLAYER_ENTERING_WORLD" then self:UnregisterEvent("PLAYER_ENTERING_WORLD") - elseif addonName == "Blizzard_CombatText" then - -- Only Blizzard_CombatText's load retires the ADDON_LOADED watch. - -- Our own addon always loads first, so unregistering on the first - -- match (the old behavior) meant the CombatTextFont object -- the - -- load-on-demand scrolling-text font -- was never restyled at its - -- actual load moment. + elseif addonName == "Blizzard_CombatText" + or not (EllesmereUIDB and EllesmereUIDB.fctFont) + or (C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("Blizzard_CombatText")) then + -- The watch exists to restyle the load-on-demand CombatTextFont + -- object at its actual load (our own addon always fires first, so + -- retiring on the first match missed it). Retire it once that has + -- happened, or when it never can: feature unused (a mid-session + -- pick needs a relog anyway) or the addon already loaded. self:UnregisterEvent("ADDON_LOADED") end end)