Repository navigation
Expand file tree
/
Copy pathCore.lua
More file actions
322 lines (286 loc) · 10.8 KB
/
Copy pathCore.lua
File metadata and controls
322 lines (286 loc) · 10.8 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
local _, BS = ...
BS.isForever = select(4, GetBuildInfo()) < 100000
BS.inCombat = false
function BS.secret(v) return issecretvalue ~= nil and issecretvalue(v) end
-- pcall wrapper for every Blizzard read; errors and secret values both come back as nil ("unknown")
function BS.try(fn, ...)
if type(fn) ~= "function" then return end
local ok, v = pcall(fn, ...)
if ok and not BS.secret(v) then return v end
end
function BS.SpellName(id) return C_Spell and BS.try(C_Spell.GetSpellName, id) end
function BS.Clamp(v, lo, hi)
if type(v) ~= "number" or v ~= v then return lo end
return math.min(hi, math.max(lo, v))
end
------------------------------------------------------------------------
-- Profile schema
------------------------------------------------------------------------
local STACK_DEFAULTS = { point = "CENTER", x = 0, y = -180, scale = 1, alpha = 1, showInCombat = true, showOutOfCombat = true }
local BAR_DEFAULTS = {
name = "New bar", offset = { x = 0, y = 0 }, size = { w = 240, h = 24 },
orientation = "horizontal", visibility = "always", spells = {},
style = { texture = "flat", color = "#C79C6E" },
}
local ENUMS = { visibility = { always = true, aura = true, proc = true }, orientation = { horizontal = true, vertical = true } }
local Clamp = BS.Clamp
-- copy defaults into t wherever a field is missing or has the wrong type
local function fill(t, defaults)
for k, v in pairs(defaults) do
if type(t[k]) ~= type(v) then
t[k] = type(v) == "table" and CopyTable(v) or v
elseif type(v) == "table" then
fill(t[k], v)
end
end
return t
end
function BS.NormalizeBar(b)
fill(b, BAR_DEFAULTS)
if type(b.id) ~= "string" then b.id = ("b%d-%d"):format(time(), math.random(1e6)) end
for k, ok in pairs(ENUMS) do
if not ok[b[k]] then b[k] = BAR_DEFAULTS[k] end
end
if not BS.TEXTURES[b.style.texture] then b.style.texture = "flat" end
b.offset.x, b.offset.y = Clamp(b.offset.x, -2000, 2000), Clamp(b.offset.y, -2000, 2000)
b.size.w, b.size.h = Clamp(b.size.w, 1, 2000), Clamp(b.size.h, 1, 2000)
local spells = {}
for _, sp in ipairs(b.spells) do
if type(sp) == "table" and type(sp.baseSpell) == "number" then
spells[#spells + 1] = {
baseSpell = sp.baseSpell,
name = type(sp.name) == "string" and sp.name or BS.SpellName(sp.baseSpell),
display = sp.display == "bar" and "bar" or "icon",
}
end
end
b.spells = spells
return b
end
function BS.NormalizeProfile(p)
fill(p, { version = 1, updated = 0, stack = STACK_DEFAULTS, bars = {} })
local s = p.stack
s.point = "CENTER" -- the only anchor we ever write
s.x, s.y = Clamp(s.x, -4000, 4000), Clamp(s.y, -4000, 4000)
s.scale, s.alpha = Clamp(s.scale, 0.3, 3), Clamp(s.alpha, 0, 1)
local bars = {}
for _, b in ipairs(p.bars) do
if type(b) == "table" then bars[#bars + 1] = BS.NormalizeBar(b) end
end
p.bars = bars
return p
end
function BS.NewBar(index)
local b = BS.NormalizeBar({})
table.insert(BS.profile.bars, index or 1, b)
return b
end
------------------------------------------------------------------------
-- Storage: every save writes all backends, load takes the newest `updated`.
-- 1. SavedVariables BarStackDB 2. account CVar chunks 3. export string (manual, Config window)
-- ponytail: Edit Mode layout store backend skipped until we know which keys are safe to write on Forever.
------------------------------------------------------------------------
function BS.Encode(t)
return BS.try(function()
local E = C_EncodingUtil
return E.EncodeBase64(E.CompressString(E.SerializeCBOR(t)))
end)
end
function BS.Decode(s)
if type(s) ~= "string" or s == "" then return end
local t = BS.try(function()
local E = C_EncodingUtil
return E.DeserializeCBOR(E.DecompressString(E.DecodeBase64(s)))
end)
return type(t) == "table" and t or nil
end
local CVAR, CVAR_COUNT, CHUNK = "barStackData", 40, 250
local cvarsOK
local function registerCVars()
if cvarsOK == nil then
cvarsOK = pcall(function()
for i = 1, CVAR_COUNT do C_CVar.RegisterCVar(CVAR .. i, "") end
end)
end
return cvarsOK
end
BS.RegisterCVars = registerCVars
local function writeCVars(s)
-- ponytail: profiles over 10KB skip this backend; SavedVariables and the export string still cover them
if not registerCVars() or #s > CHUNK * CVAR_COUNT then return end
for i = 1, CVAR_COUNT do
pcall(C_CVar.SetCVar, CVAR .. i, s:sub((i - 1) * CHUNK + 1, i * CHUNK))
end
end
local function readCVars()
if not registerCVars() then return end
local parts = {}
for i = 1, CVAR_COUNT do
local v = BS.try(C_CVar.GetCVar, CVAR .. i)
if type(v) ~= "string" or v == "" then break end
parts[i] = v
end
return BS.Decode(table.concat(parts))
end
function BS.LoadDB()
local sv = type(BarStackDB) == "table" and BarStackDB or nil
local cv = readCVars()
BS.svFound, BS.cvFound = sv ~= nil, cv ~= nil
local db = sv
if cv and (not sv or (tonumber(cv.updated) or 0) > (tonumber(sv.updated) or 0)) then db = cv end
db = db or {}
if type(db.profiles) ~= "table" then db.profiles = {} end
BS.db, BarStackDB = db, db
end
function BS.Save()
BS.db.updated = time()
BS.profile.updated = BS.db.updated
BarStackDB = BS.db
local s = BS.Encode(BS.db)
if s then writeCVars(s) end
end
local function profileKey()
local spec = BS.try(GetSpecialization) or 0
return ("%s-%s:%d"):format(UnitName("player"), GetRealmName(), spec)
end
function BS.LoadProfile()
local key = profileKey()
local p = BS.db.profiles[key]
if type(p) ~= "table" then p = {} end
BS.profile = BS.NormalizeProfile(p)
BS.db.profiles[key] = BS.profile
end
function BS.Export() return BS.Encode(BS.profile) end
function BS.Import(s)
local p = BS.Decode(type(s) == "string" and (s:gsub("%s", "")) or nil)
if not p or p.version ~= 1 or type(p.bars) ~= "table" then return false end
BS.db.profiles[profileKey()] = p
BS.LoadProfile()
BS.Save()
BS.Apply()
return true
end
------------------------------------------------------------------------
-- Apply: layout changes wait for combat to end
------------------------------------------------------------------------
function BS.Apply()
if InCombatLockdown() then BS.pending = true return end
BS.pending = false
BS.Layout()
end
function BS.Changed()
BS.Save()
BS.Apply()
end
function BS.Check()
local function line(ok, text) print((ok and "|cff40ff40OK|r " or "|cffff4040--|r ") .. text) end
print(("|cffC79C6EBar Stack|r check: %s client, interface %d"):format(BS.isForever and "Forever" or "Midnight", select(4, GetBuildInfo())))
line(BS.svFound, "SavedVariables BarStackDB existed at login")
line(BS.cvFound, "CVar backend returned saved data at login")
line(C_EncodingUtil ~= nil, "C_EncodingUtil (export string, CVar backend)")
line(registerCVars(), "custom CVars registered")
line(BS.try(C_CVar.GetCVar, "cooldownViewerEnabled") == "1", "Cooldown Manager enabled")
line(issecretvalue ~= nil, "issecretvalue present")
line(EventRegistry ~= nil, "Edit Mode callbacks (EventRegistry)")
for _, name in ipairs(BS.ALL_VIEWERS) do
local total, mapped = BS.ViewerStats(_G[name])
line(mapped > 0, ("%s: %s, %d frames, %d mapped to spells"):format(name, _G[name] and "found" or "missing", total, mapped))
end
end
local function cdmPrompt()
if BS.db.cdmPrompted or BS.try(C_CVar.GetCVar, "cooldownViewerEnabled") ~= "0" then return end
BS.db.cdmPrompted = true
StaticPopupDialogs.BARSTACK_CDM = {
text = "Bar Stack builds its bars from Blizzard's Cooldown Manager, which is turned off.\n\nEnable it in Options > Gameplay Enhancements.",
button1 = OKAY, timeout = 0, whileDead = true, hideOnEscape = true,
}
StaticPopup_Show("BARSTACK_CDM")
end
------------------------------------------------------------------------
-- Events, slash command, addon compartment
------------------------------------------------------------------------
-- ponytail: hand-rolled LibDBIcon look-alike; assumes a round minimap, buttons sit off the edge of square ones
local function minimapButton()
local b = CreateFrame("Button", "BarStackMinimapButton", Minimap)
b:SetSize(31, 31)
b:SetFrameStrata("MEDIUM")
b:SetFrameLevel(8)
b:RegisterForClicks("AnyUp")
b:RegisterForDrag("LeftButton")
b:SetHighlightTexture(136477) -- UI-Minimap-ZoomButton-Highlight
local bg = b:CreateTexture(nil, "BACKGROUND")
bg:SetSize(20, 20)
bg:SetPoint("TOPLEFT", 7, -5)
bg:SetTexture(136467) -- UI-Minimap-Background
local icon = b:CreateTexture(nil, "ARTWORK")
icon:SetSize(17, 17)
icon:SetPoint("TOPLEFT", 7, -6)
icon:SetTexture("Interface\\Icons\\Ability_Warrior_OffensiveStance")
local border = b:CreateTexture(nil, "OVERLAY")
border:SetSize(50, 50)
border:SetPoint("TOPLEFT")
border:SetTexture(136430) -- MiniMap-TrackingBorder
local function place()
local a, r = math.rad(BS.db.minimapAngle or 225), Minimap:GetWidth() / 2 + 5
b:ClearAllPoints()
b:SetPoint("CENTER", Minimap, "CENTER", r * math.cos(a), r * math.sin(a))
end
place()
b:SetScript("OnClick", BS.ToggleConfig)
b:SetScript("OnDragStart", function()
b:SetScript("OnUpdate", function()
local mx, my = Minimap:GetCenter()
local cx, cy = GetCursorPosition()
local s = Minimap:GetEffectiveScale()
BS.db.minimapAngle = math.deg(math.atan2(cy / s - my, cx / s - mx))
place()
end)
end)
b:SetScript("OnDragStop", function()
b:SetScript("OnUpdate", nil)
BS.Save()
end)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(b, "ANCHOR_LEFT")
GameTooltip:AddLine("Bar Stack")
GameTooltip:AddLine("Click to open settings. Drag to move.", 1, 1, 1)
GameTooltip:Show()
end)
b:SetScript("OnLeave", GameTooltip_Hide)
end
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("ADDON_LOADED")
ev:RegisterEvent("PLAYER_REGEN_DISABLED")
ev:RegisterEvent("PLAYER_REGEN_ENABLED")
ev:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
ev:SetScript("OnEvent", function(_, event, arg)
if event == "PLAYER_LOGIN" then
BS.LoadDB()
BS.LoadProfile()
BS.Init()
BS.Apply()
minimapButton()
cdmPrompt()
elseif event == "ADDON_LOADED" then
if arg == "Blizzard_CooldownViewer" and BS.profile then BS.Apply() end
elseif event == "PLAYER_REGEN_DISABLED" then
BS.inCombat = true
BS.UpdateVisibility()
elseif event == "PLAYER_REGEN_ENABLED" then
BS.inCombat = false
if BS.pending then BS.Apply() else BS.UpdateVisibility() end
if BS.RefreshConfig then BS.RefreshConfig() end
elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
if arg == "player" and BS.profile then
BS.LoadProfile()
BS.Apply()
if BS.RefreshConfig then BS.RefreshConfig() end
end
end
end)
SLASH_BARSTACK1, SLASH_BARSTACK2 = "/bs", "/barstack"
SlashCmdList.BARSTACK = function(msg)
if strtrim(msg or ""):lower() == "check" then BS.Check() else BS.ToggleConfig() end
end
function BarStack_OnCompartmentClick() BS.ToggleConfig() end