forked from EllesmereGaming/EllesmereUI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEllesmereUI_Startup.lua
More file actions
419 lines (388 loc) · 19.9 KB
/
Copy pathEllesmereUI_Startup.lua
File metadata and controls
419 lines (388 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
-------------------------------------------------------------------------------
-- EllesmereUI_Startup.lua
-- Runs as early as possible (first file after the Lite framework).
-- Applies settings that the WoW engine caches at login time, before
-- other addon files or PLAYER_LOGIN handlers have a chance to run.
-------------------------------------------------------------------------------
local ADDON_NAME = ...
-------------------------------------------------------------------------------
-- Pixel-Perfect UI Scale
--
-- SavedVariables (EllesmereUIDB) aren't available at file scope — they load
-- at ADDON_LOADED. So we use events:
-- ADDON_LOADED -> DB is available. If we have a saved scale, apply it.
-- PLAYER_ENTERING_WORLD -> Blizzard has applied the user's CVar scale.
-- If no saved scale yet (first install / reset), snapshot
-- the user's current Blizzard scale and save it.
-------------------------------------------------------------------------------
do
local GetPhysicalScreenSize = GetPhysicalScreenSize
local dbReady = false
local scaleKnown = false -- true when ppUIScale was already saved
local function ApplyScaleSafe(scale)
if InCombatLockdown() then
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:SetScript("OnEvent", function(self)
self:UnregisterEvent("PLAYER_REGEN_ENABLED")
UIParent:SetScale(scale)
if EllesmereUI and EllesmereUI.PP and EllesmereUI.PP.UpdateMult then
EllesmereUI.PP.UpdateMult()
end
end)
else
UIParent:SetScale(scale)
if EllesmereUI and EllesmereUI.PP and EllesmereUI.PP.UpdateMult then
EllesmereUI.PP.UpdateMult()
end
end
end
local function SyncMultOnly()
if EllesmereUI and EllesmereUI.PP then
if EllesmereUI.PP.UpdateMult then EllesmereUI.PP.UpdateMult() end
if EllesmereUI.PP.ResnapAllBorders then EllesmereUI.PP.ResnapAllBorders() end
end
end
local scaleFrame = CreateFrame("Frame")
scaleFrame:RegisterEvent("ADDON_LOADED")
scaleFrame:RegisterEvent("PLAYER_LOGIN")
scaleFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
scaleFrame:SetScript("OnEvent", function(self, event, addonName)
if event == "ADDON_LOADED" then
if addonName ~= ADDON_NAME then return end
self:UnregisterEvent("ADDON_LOADED")
dbReady = true
if not EllesmereUIDB then EllesmereUIDB = {} end
local _, physH = GetPhysicalScreenSize()
local perfect = 768 / physH
local function PixelBestSize()
return max(0.4, min(perfect, 1.15))
end
if EllesmereUIDB.ppUIScale then
-- Migrate 0.53 to exact pixel-perfect 0.5333... (768/1440)
if EllesmereUIDB.ppUIScale == 0.53 then
EllesmereUIDB.ppUIScale = 0.5333333333
-- Migrate 0.71 to exact pixel-perfect 0.7111... (768/1080)
elseif EllesmereUIDB.ppUIScale == 0.71 then
EllesmereUIDB.ppUIScale = 0.7111111111
end
scaleKnown = true
-- Apply here, not only at PLAYER_LOGIN: the engine restores
-- user-placed frame positions from its own layout cache during
-- login, converting the stored values with UIParent's scale AT
-- THAT MOMENT. The cache was written at last logout using OUR
-- scale, so applying ours later than the restore makes the
-- round-trip asymmetric and every user-placed frame shifts by
-- (ourScale / cvarScale) every session -- the undocked chat
-- window drift, field-measured at exactly x0.8333 =
-- 0.5333/0.64 per reload. ADDON_LOADED is the earliest point
-- the saved value exists, so applying it here closes the
-- window: the engine decodes with the scale that encoded.
-- The PLAYER_LOGIN apply below stays as an idempotent belt.
--
-- FIELD RESULT (2026-07-28): this did NOT stop the drift.
-- Blizzard applies the user's CVar scale during login AFTER
-- addon ADDON_LOADED (this file's own PLAYER_ENTERING_WORLD
-- comment says so), so the chat restore still ran at the CVar
-- scale. Kept anyway: it is idempotent, costs nothing, and
-- closes the same window for anything restored before
-- Blizzard's CVar apply. The chat fix is below.
ApplyScaleSafe(EllesmereUIDB.ppUIScale)
end
elseif event == "PLAYER_LOGIN" then
self:UnregisterEvent("PLAYER_LOGIN")
if scaleKnown and EllesmereUIDB.ppUIScale then
-- Returning user: single SetScale at PLAYER_LOGIN.
-- No timers, no repeated calls.
ApplyScaleSafe(EllesmereUIDB.ppUIScale)
-- Re-apply our scale whenever Blizzard fires UI_SCALE_CHANGED
-- (zone transitions, CVar resets, resolution changes).
self:RegisterEvent("UI_SCALE_CHANGED")
return
end
-- First-time path: just sync mult for child addon OnEnable
if EllesmereUI and EllesmereUI.PP and EllesmereUI.PP.UpdateMult then
EllesmereUI.PP.UpdateMult()
end
elseif event == "UI_SCALE_CHANGED" then
local saved = EllesmereUIDB and EllesmereUIDB.ppUIScale
if saved then
ApplyScaleSafe(saved)
SyncMultOnly()
end
return
elseif event == "PLAYER_ENTERING_WORLD" then
self:UnregisterEvent("PLAYER_ENTERING_WORLD")
if not dbReady then return end
if not EllesmereUIDB then EllesmereUIDB = {} end
-- Returning user: scale was applied once at PLAYER_LOGIN,
-- nothing else needed.
if scaleKnown then return end
-- First install or reset: snapshot the user's Blizzard scale
if EllesmereUIDB.ppUIScale == nil then
local blizzScale = UIParent:GetScale()
local clamped = max(0.4, min(blizzScale, 1.15))
EllesmereUIDB.ppUIScale = clamped
EllesmereUIDB.ppUIScaleAuto = false
-- Seed the options-panel scale from the display height. The
-- panel is deliberately pinned to physical pixels (baseScale =
-- GetScreenWidth()/physW) so it holds a constant physical size
-- and does NOT follow the UI Scale slider. At 1080p that reads
-- fine, but on a 4K screen the same pixel count covers half as
-- much of the display: the panel arrives unreadably small and
-- the UI Scale slider appears to do nothing to it.
--
-- 1440p is the reference look: a panel of H units covers
-- H*panelScale/physH of the screen, so physH/1440 reproduces
-- 1440p's screen fraction on any display, and 4K seeds 1.5 to
-- read exactly like a 2K monitor. Floored at 1 so 1080p (which
-- runs a slightly larger fraction, uncomplained-about) keeps
-- its current size rather than shrinking, then snapped onto the
-- dropdown's real steps (see EllesmereUI.SnapPanelScale) -- an
-- off-menu value leaves the control reading 100% while the
-- panel renders larger.
--
-- This sits INSIDE the ppUIScale == nil guard on purpose: it is
-- the first-install path only. Existing saves never reach here
-- (returning users return at scaleKnown above) and are seeded by
-- the panel_scale_highdpi_reset_v3 migration instead.
if EllesmereUIDB.panelScale == nil then
local _, physH = GetPhysicalScreenSize()
if type(physH) == "number" and physH > 0 then
local seeded = max(1, min(physH / 1440, 2))
EllesmereUIDB.panelScale =
(EllesmereUI and EllesmereUI.SnapPanelScale
and EllesmereUI.SnapPanelScale(seeded)) or seeded
end
end
end
local scale = EllesmereUIDB.ppUIScale
if not scale then return end
-- First-time install: apply scale with safety net.
-- Apply scale multiple times to guarantee it sticks even on
-- slow machines where Blizzard may reset it during init.
if EllesmereUI and EllesmereUI.PP and EllesmereUI.PP.UpdateMult then
EllesmereUI.PP.UpdateMult()
end
ApplyScaleSafe(scale)
C_Timer.After(2, function()
if InCombatLockdown() then return end
if EllesmereUIDB and EllesmereUIDB.ppUIScale then
ApplyScaleSafe(EllesmereUIDB.ppUIScale)
end
SyncMultOnly()
end)
C_Timer.After(5, function()
if InCombatLockdown() then return end
if EllesmereUIDB and EllesmereUIDB.ppUIScale then
ApplyScaleSafe(EllesmereUIDB.ppUIScale)
end
SyncMultOnly()
end)
end
end)
end
-- Apply the saved unit name font -- the names that float above players, NPCs
-- and enemies. UNIT_NAME_FONT is a plain path string the engine reads once at
-- login, so a UI reload is not enough and the change shows after a full relog.
--
-- Opt-in: while no name font is chosen the global is never written, so
-- Blizzard's default is left exactly as it was.
--
-- Rides the combat text font's event frame below rather than creating its own.
-- Both drive a Blizzard global cached at login and want the same timing, and a
-- feature nobody enabled must not register events or build frames of its own.
--
-- 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.
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
path = EllesmereUI.ResolveFontName(name)
end
if type(path) == "string" and path ~= "" then
_G.UNIT_NAME_FONT = path
end
end
-------------------------------------------------------------------------------
-- Undocked chat window position fix
--
-- Root cause, arithmetically pinned by the field drift capture (2026-07-28).
-- UIParent's height in UI units is always 768 / scale, so it is 1440 at our
-- pixel-perfect 0.5333 but 1200 at the tester's CVar scale 0.64. Blizzard
-- stores an undocked window's position as a screen-height RATIO and restores
-- it as ratio * GetScreenHeight(). Blizzard applies the CVar scale during
-- login and we apply ours at PLAYER_LOGIN, AFTER the chat restore has
-- already run -- so the restore resolves the ratio against the 1200 space:
-- correct : 0.1566 * 1440 = 225.5 (where the user dropped it)
-- restored: 0.1566 * 1200 = 187.9 (where it reappeared)
-- Then we rescale UIParent to the 1440 space and the frame keeps that
-- numeric 188 offset, which now points somewhere lower. Each session
-- repeats it, so the window creeps toward the bottom-left by
-- (cvarScale height / our height) per login. Only setups whose EUI scale
-- differs from the CVar scale drift, which is why not everyone sees it.
--
-- Fixing the scale TIMING does not work: applying our scale at ADDON_LOADED
-- (kept above, harmless) is overwritten by Blizzard's own CVar apply later
-- in login, so the restore still runs at the CVar scale. The position has
-- to be recomputed after both the restore and our final scale, which is what
-- this pass does -- Blizzard's own formula, against the settled space.
--
-- TAINT NOTE -- anchoring a Blizzard chat frame from insecure code is the
-- injector class this module's bisect ledger convicted, so this pass was
-- suspected of causing the field ChatFrameEditBox.lua:360 secret-SetText
-- error and was removed entirely in v6. The error reproduced on v6, a build
-- whose only chat contact is read-only getters -- so the pass is NOT the
-- vector and is restored here. That error is tracked separately as a
-- pre-existing chat-module issue. Exposure is still kept minimal: ONE
-- deferred pass per login, never during a session, no hooks, and nothing
-- written to Blizzard frame state (SetPoint only). Do not add the
-- FCF_SavePositionAndDimensions hook, the SetUserPlaced writes, or the
-- UPDATE_CHAT_WINDOWS registration back -- all three were separately
-- pulled, none of them bought anything.
-------------------------------------------------------------------------------
do
local function ReassertUndockedPositions()
if not GetChatWindowSavedPosition then return end
local W, H = GetScreenWidth(), GetScreenHeight()
if not (W and H) then return end
for i = 2, NUM_CHAT_WINDOWS or 10 do
local cf = _G["ChatFrame" .. i]
if cf and cf:IsShown() and not cf.isDocked and not cf.isTemporary then
local point, xOff, yOff = GetChatWindowSavedPosition(i)
if point and xOff and yOff then
-- Blizzard's own restore formula, re-run now that the
-- scale (and therefore GetScreenWidth/Height) has settled.
cf:ClearAllPoints()
cf:SetPoint(point, UIParent, point, xOff * W, yOff * H)
end
end
end
end
local fixFrame = CreateFrame("Frame")
fixFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
fixFrame:SetScript("OnEvent", function(self, _, initialLogin, reloadingUi)
if not (initialLogin or reloadingUi) then return end
self:UnregisterEvent("PLAYER_ENTERING_WORLD")
-- Obsolete v2 migration flag; the rebase it gated was a no-op.
if EllesmereUIDB then EllesmereUIDB.chatPosRebased = nil end
C_Timer.After(0, ReassertUndockedPositions)
-- Belt for slow loads, still inside the login window.
C_Timer.After(2, ReassertUndockedPositions)
end)
end
-- Apply the saved combat text font immediately at file scope.
-- DAMAGE_TEXT_FONT must be set before the engine caches it at login.
-- 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
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
end
_G.DAMAGE_TEXT_FONT = fontPath
if _G.CombatTextFont then
_G.CombatTextFont:SetFont(fontPath, 120, "")
end
end
-- Apply immediately (sets DAMAGE_TEXT_FONT before engine caches it)
ApplyCombatTextFont()
-- Re-apply on ADDON_LOADED (our addon or Blizzard_CombatText), PLAYER_LOGIN,
-- and PLAYER_ENTERING_WORLD to cover all timing windows where the engine
-- may cache or reset the combat text font.
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:SetScript("OnEvent", function(self, event, addonName)
if event == "ADDON_LOADED" then
if addonName ~= ADDON_NAME and addonName ~= "Blizzard_CombatText" then
return
end
end
ApplyCombatTextFont()
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
self:UnregisterEvent("ADDON_LOADED")
end
end)
end
-- NOTE: the global _G.STANDARD_TEXT_FONT override that used to live here was
-- removed. It was gated on the "Reskin Blizzard Elements" (customTooltips) toggle
-- and read a dead legacy key (EllesmereUIDB.fontSettings.global), so it always
-- forced STANDARD_TEXT_FONT to the bundled Expressway.TTF -- a Latin-only face --
-- regardless of the user's actual font choice. In CJK/Cyrillic locales that broke
-- glyphs across the whole Blizzard UI AND other addons (square boxes), because it
-- bypassed the locale-aware ResolveFontName fallback.
--
-- Changing the global game-text font is now handled exclusively by the opt-in,
-- locale-aware EllesmereUI.ApplyGlobalFontToGameText() ("Apply to All Game Text"),
-- which runs once at PLAYER_LOGIN. Reskinned Blizzard elements still pick up the
-- EllesmereUI font on their own via per-element, locale-aware SetFont calls
-- (EllesmereUI.GetFontPath("blizzardSkin")), so reskinning no longer touches the
-- global font and never affects other addons.
-------------------------------------------------------------------------------
-- Auto-disable EllesmereUIBags when a dedicated bag addon is present.
-- Once the user manually toggles the Bags module (sidebar power button or
-- first-install popup), we set EllesmereUIDB.bagsUserChosen and never
-- override their preference again.
-------------------------------------------------------------------------------
do
local BAG_ADDONS = {
"AdiBags", "ArkInventory", "Baganator", "Bagnon", "BetterBags", "Sorted",
}
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(self, event, addonName)
if addonName ~= ADDON_NAME then return end
self:UnregisterAllEvents()
if not EllesmereUIDB then EllesmereUIDB = {} end
if EllesmereUIDB.bagsUserChosen then return end
if not C_AddOns or not C_AddOns.GetAddOnEnableState then return end
-- If we previously auto-disabled bags but the user re-enabled it
-- (via Blizzard addon list or any other means), respect their choice.
local bagsEnabled = C_AddOns.GetAddOnEnableState("EllesmereUIBags") > 0
if EllesmereUIDB.bagsAutoDisabled and bagsEnabled then
EllesmereUIDB.bagsUserChosen = true
EllesmereUIDB.bagsAutoDisabled = nil
return
end
for _, name in ipairs(BAG_ADDONS) do
if C_AddOns.GetAddOnEnableState(name) > 0 then
C_AddOns.DisableAddOn("EllesmereUIBags")
EllesmereUIDB.bagsAutoDisabled = true
return
end
end
EllesmereUIDB.bagsAutoDisabled = nil
end)
end
-- (The DataBars auto-disable block was removed 2026-07-13: after the
-- multi-bar rewrite the module does literally nothing until the user
-- creates a bar, so it ships enabled with zero cost. If a prior build
-- auto-disabled it, re-enabling once sticks -- the latch keys
-- dataBarsAutoDisabled/dataBarsUserChosen are simply no longer read.)
-- /rl reload shortcut -- only
if not SlashCmdList["RL"] then
SlashCmdList["RL"] = function() ReloadUI() end
SLASH_RL1 = "/rl"
end