Repository navigation
Expand file tree
/
Copy pathInit.lua
More file actions
584 lines (535 loc) · 20.6 KB
/
Copy pathInit.lua
File metadata and controls
584 lines (535 loc) · 20.6 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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
--INITIALIZE
local Self = LibStub("AceAddon-3.0"):NewAddon(
"SlackHacks",
"AceConsole-3.0",
"AceEvent-3.0",
"AceComm-3.0",
"AceSerializer-3.0"
)
Self.config = LibStub("AceConfig-3.0")
Self.frame = CreateFrame("Frame", "SlackHacks")
Self.itemBindingFrame = CreateFrame("Frame", "SlackHacks Item Bindings")
_G.SlackHacks = Self
Self.Self = Self
setmetatable(Self, {__index = _G}) -- The global environment is now checked if a key is not found in addon
if _G.ColorMixin then Self.ColorMixin = _G.ColorMixin end
setfenv(1, Self) -- Namespace local to addon
addonName, addonTable = ...
SLACKHACKS_ICON = "Interface\\AddOns\\SlackHacks\\Assets\\SlackHacks.png"
CONFIG_VERSION = 9
-- The addon release (`## Version` in SlackHacks.toc) that first ships each config version, so any addon at or
-- above that release requires the matching CONFIG_MIGRATIONS entry. Development bookkeeping only; nothing reads
-- it at runtime. Config version 1 covers every release before v2. Add an entry whenever CONFIG_VERSION is bumped.
CONFIG_VERSION_RELEASES = {
[1] = 1,
[2] = 2,
[3] = 4,
[4] = 7,
[5] = 10, -- Config v5-v7 predate the once-per-release rule and all shipped together.
[6] = 10,
[7] = 10,
[8] = 12,
[9] = 13,
}
function getAddonVersion()
return tostring((C_AddOns and C_AddOns.GetAddOnMetadata and C_AddOns.GetAddOnMetadata("SlackHacks", "Version"))
or (GetAddOnMetadata and GetAddOnMetadata("SlackHacks", "Version"))
or "1")
end
Enum.SelfVendorMode = {
CONSUMABLES = 1,
OIL = 4,
AUGMENT_RUNES = 5,
VANTUS_RUNE = 6,
AUGMENTS = 7,
FLASK = 8,
CRIT_FLASK = 9,
MASTERY_FLASK = 10,
HASTE_FLASK = 11,
VERSATILITY_FLASK = 12,
}
function getBattletag()
return select(2, BNGetInfo())
end
SLACKWISE_BATTLETAG = "Slackwise#1121"
function isSlackwise()
return getBattletag() == SLACKWISE_BATTLETAG or false
end
-- Gatekeeping new features with no UI
function isTester()
-- Use a checkbox in settings later, probably, but right now it's just me
return isSlackwise()
end
function isRetail()
-- Official way Blizzard distinguishes between game clients: https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID
if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then
return true
else
return false
end
end
function isForever()
-- Official way Blizzard distinguishes between game clients: https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID
if WOW_PROJECT_ID == WOW_PROJECT_CAMELOT then
return true
else
return false
end
end
function isClassic()
-- Official way Blizzard distinguishes between game clients: https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID
if WOW_PROJECT_ID == WOW_PROJECT_CLASSIC then
return true
else
return false
end
end
--- Returns whether this is a client with Retail's modern UI: Retail itself, or WoW Forever, which is built on the
--- Retail client and ships the same FrameXML (DamageMeter, Objective Tracker, Blizzard_Menu, ScrollBox, UnitPopup).
---@return boolean modern True on Retail or Forever.
function isRetailOrForever()
return isRetail() or isForever()
end
function getGameType()
if isRetail() then
return "RETAIL"
elseif isForever() then
return "FOREVER"
elseif isClassic() then
return "CLASSIC"
else
return "UNKNOWN" -- Uh oh
end
end
COLOR_START = "\124c"
COLOR_END = "\124r"
function color(color)
return function(text)
return COLOR_START .. "FF" .. color .. text .. COLOR_END
end
end
grey = color("AAAAAA")
function icon(size)
return "\124T" .. SLACKHACKS_ICON .. ":" .. (size or 16) .. "\124t"
end
-- Maps a target config version to the function that migrates from (target - 1) to it.
-- CONFIG_VERSION_RELEASES records the addon release that first ships each one; the "Config vN, addon vX"
-- comments below mirror it.
--
-- Bump CONFIG_VERSION at most ONCE per release. If CONFIG_VERSION_RELEASES[CONFIG_VERSION] is already the
-- upcoming (unreleased) addon version, extend that entry instead of bumping again; otherwise bump it, add the
-- new migration here, and map it to the upcoming release in CONFIG_VERSION_RELEASES.
-- (Config v5-v7 predate this rule and all shipped together in addon v10.)
CONFIG_MIGRATIONS = {
-- Config v2, shipped in addon v2.
[2] = function()
local function migrateProfile(p)
if not p or type(p) ~= "table" then return end
p.listviewBag = p.listviewBag or {}
if p.listviewBag.trackCrestCurrencies == nil then
p.listviewBag.trackCrestCurrencies = true
end
if type(p.minimapIcon) == "boolean" then
p.minimapIcon = {
enabled = p.minimapIcon,
minimapPos = 220,
}
elseif p.minimapIcon == nil then
p.minimapIcon = {
enabled = true,
minimapPos = 220,
}
elseif type(p.minimapIcon) == "table" then
if p.minimapIcon.enabled == nil then
p.minimapIcon.enabled = true
end
if p.minimapIcon.minimapPos == nil then
p.minimapIcon.minimapPos = 220
end
end
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do
migrateProfile(p)
end
end
if Self.db.profile then
migrateProfile(Self.db.profile)
end
-- Default error logging to true in version 2
local g = Self.db.global
if g then
g.errorMinimap = nil
g.logs = g.logs or {}
g.logs.errorLoggingEnabled = true
if Self.Debug and Self.Debug.RefreshErrorLogging then
Self.Debug:RefreshErrorLogging()
end
if updateMinimapIcon then
updateMinimapIcon()
elseif Self.UpdateMinimapIcon then
Self.UpdateMinimapIcon()
end
end
end,
-- Config v3, shipped in addon v4.
[3] = function()
-- Purge previous error logs for version 3
local g = Self.db.global
if g and g.logs and g.logs.error then
wipe(g.logs.error)
end
end,
-- Config v4, shipped in addon v7.
[4] = function()
-- Add the Quest / NPC module settings, all off by default.
local QUEST_DEFAULTS = {
enabled = false,
autoAccept = false,
autoComplete = false,
autoClickFirstOption = false,
numberKeys = false,
questItemButton = false,
}
local function migrateProfile(p)
if not p or type(p) ~= "table" then return end
if type(p.quest) ~= "table" then
p.quest = {}
end
for key, value in pairs(QUEST_DEFAULTS) do
if p.quest[key] == nil then
p.quest[key] = value
end
end
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do
migrateProfile(p)
end
end
if Self.db.profile then
migrateProfile(Self.db.profile)
end
end,
-- Config v5, shipped in addon v10.
[5] = function()
-- Rename movableWindows -> windows and split it into nested `movable` and `scaling` tables. The old
-- single `enabled` switch becomes both the overall Windows switch and the Movable Windows switch, so
-- existing users keep the exact behavior they had.
local function migrateProfile(p)
if not p or type(p) ~= "table" then return end
local old = p.movableWindows
if type(old) ~= "table" then return end
local enabled = old.enabled == true
-- Merge into any existing `windows` table (AceDB may already have filled defaults into the current
-- profile) and only copy values that were actually saved: AceDB strips default values on logout, so
-- old fields are often nil, and every table the module indexes must exist afterwards.
local w = type(p.windows) == "table" and p.windows or {}
p.windows = w
w.enabled = enabled
w.movable = type(w.movable) == "table" and w.movable or {}
w.scaling = type(w.scaling) == "table" and w.scaling or {}
local function copy(dst, key, value)
if value ~= nil then dst[key] = value end
end
w.movable.enabled = enabled
copy(w.movable, "moveToasts", old.moveToasts)
copy(w.movable, "modifierKey", old.modifierKey)
copy(w.movable, "savePositionStrategy", old.savePositionStrategy)
copy(w.movable, "toastPoints", old.toastPoints)
w.movable.points = old.points or w.movable.points or {}
copy(w.scaling, "enabled", old.enableScaling)
copy(w.scaling, "modifierKey", old.scaleModifierKey)
copy(w.scaling, "saveStrategy", old.saveScaleStrategy)
w.scaling.scales = old.scales or w.scaling.scales or {}
p.movableWindows = nil
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do
migrateProfile(p)
end
end
if Self.db.profile then
migrateProfile(Self.db.profile)
end
end,
-- Config v6, shipped in addon v10.
[6] = function()
-- Toast moving and its saved position move up from windows.movable to the Windows level, and Zone Map
-- scaling gets its own switch (defaulting on, since it was previously always active).
local function migrateProfile(p)
if not p or type(p) ~= "table" or type(p.windows) ~= "table" then return end
local w = p.windows
if type(w.movable) == "table" then
-- A value saved under movable always wins: AceDB may already have copied a default into `windows`.
if w.movable.moveToasts ~= nil then w.moveToasts = w.movable.moveToasts end
if w.movable.toastPoints ~= nil then w.toastPoints = w.movable.toastPoints end
w.movable.moveToasts = nil
w.movable.toastPoints = nil
end
if rawget(w, "zoneMapScaling") == nil then w.zoneMapScaling = true end
w.movable = type(w.movable) == "table" and w.movable or {}
w.movable.points = w.movable.points or {}
w.scaling = type(w.scaling) == "table" and w.scaling or {}
w.scaling.scales = w.scaling.scales or {}
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do
migrateProfile(p)
end
end
if Self.db.profile then
migrateProfile(Self.db.profile)
end
end,
-- Config v7, shipped in addon v10.
[7] = function()
-- Rename windows.moveToasts -> windows.movableToasts and windows.zoneMapScaling -> windows.scalingZoneMap.
-- Only saved (raw) values are moved, so AceDB defaults never overwrite a user's choice.
local function migrateProfile(p)
if not p or type(p) ~= "table" or type(p.windows) ~= "table" then return end
local w = p.windows
if rawget(w, "moveToasts") ~= nil then
w.movableToasts = rawget(w, "moveToasts")
w.moveToasts = nil
end
if rawget(w, "zoneMapScaling") ~= nil then
w.scalingZoneMap = rawget(w, "zoneMapScaling")
w.zoneMapScaling = nil
end
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do
migrateProfile(p)
end
end
if Self.db.profile then
migrateProfile(Self.db.profile)
end
end,
-- Config v8, addon v12.
--- Adds the opt-in Spacebar shortcut to every saved profile.
[8] = function()
--- Adds the opt-in Spacebar shortcut without changing existing quest settings.
---@param p table|nil Saved profile.
local function migrateProfile(p)
if type(p) ~= "table" then return end
if type(p.quest) ~= "table" then p.quest = {} end
if p.quest.spacebar == nil then p.quest.spacebar = false end
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do migrateProfile(p) end
end
migrateProfile(Self.db.profile)
end,
-- Config v9, addon v13.
--- Moves the renamed Guild Member List's settings (infoDisplay.guildMemberList) to the Guildie List
--- (infoDisplay.guildieList) in every saved profile, and splits Gear Stats from the Info Display's switch.
[9] = function()
--- Carries over the profile's saved Guild Member List settings under the Guildie List's key, and moves the
--- Info Display's old on/off state to Gear Stats.
---@param p table|nil Saved profile.
local function migrateProfile(p)
if type(p) ~= "table" then return end
-- Before v13 `infoDisplay.enabled` (default false) only meant "Gear Stats", kept in sync with
-- charSheet.gearStats. It's now the master switch for every Info Display module (default true), so a profile
-- that had it off keeps Gear Stats hidden instead and the other modules follow their own settings.
-- Non-raw read, so the active profile gets AceDB's defaults-backed table; other profiles get a new one.
local rawInfoDisplay = rawget(p, "infoDisplay")
local wasEnabled = type(rawInfoDisplay) == "table" and rawget(rawInfoDisplay, "enabled") == true
if not wasEnabled then
local display = p.infoDisplay
if type(display) ~= "table" then
display = {}
p.infoDisplay = display
end
if type(display.charSheet) ~= "table" then display.charSheet = {} end
display.charSheet.gearStats = false
-- Clear an explicit false so the new default (on) applies. A cleared value reads as on when the active
-- profile is also iterated in Self.db.profiles; re-running this then only re-hides Gear Stats, so it's safe.
display.enabled = nil
end
local infoDisplay = rawget(p, "infoDisplay")
local old = type(infoDisplay) == "table" and rawget(infoDisplay, "guildMemberList")
if type(old) ~= "table" then return end
local new = infoDisplay.guildieList
if type(new) ~= "table" then
new = {}
infoDisplay.guildieList = new
end
-- Only the raw (user-saved) values are copied, so unset keys keep falling back to the defaults.
for key, value in pairs(old) do new[key] = value end
infoDisplay.guildMemberList = nil
end
if Self.db.profiles then
for _, p in pairs(Self.db.profiles) do migrateProfile(p) end
end
migrateProfile(Self.db.profile)
end,
}
-- Version 1 predates this migration system entirely (no data shape changed getting to it), so it's the
-- only version allowed to have no migration function without being treated as unrecoverable.
CONFIG_RESET_POPUP = "SLACKHACKS_CONFIG_RESET"
StaticPopupDialogs[CONFIG_RESET_POPUP] = {
text = "SlackHacks: your saved settings were reset to defaults because they were out of date or could not be loaded. Please review your settings.",
button1 = OKAY,
timeout = 0,
whileDead = true,
hideOnEscape = true,
showAlert = true,
}
RESET_PROFILE_CONFIRM_POPUP = "SLACKHACKS_RESET_PROFILE_CONFIRM"
StaticPopupDialogs[RESET_PROFILE_CONFIRM_POPUP] = {
text = "Are you sure you want to reset all SlackHacks settings for the current profile? This will wipe your profile settings and reset the addon (and may reload your UI).",
button1 = YES,
button2 = NO,
OnAccept = function()
resetCurrentProfileAndAddon()
end,
timeout = 0,
whileDead = true,
hideOnEscape = true,
showAlert = true,
}
function notifyConfigReset()
StaticPopup_Show(CONFIG_RESET_POPUP)
end
function promptResetProfile()
StaticPopup_Show(RESET_PROFILE_CONFIRM_POPUP)
end
-- Resets the current profile and reinitializes the entire addon state.
-- If an error occurs during reinitialization, it falls back to reloading the UI.
function resetCurrentProfileAndAddon()
local ok, err = pcall(function()
if Self.db then
Self.db:ResetProfile()
Self.db.global.configVersion = CONFIG_VERSION
Self.db.global.addonVersion = getAddonVersion()
end
-- Reset addon state by disabling and re-enabling the addon and its modules
Self:Disable()
Self:Enable()
local acr = LibStub("AceConfigRegistry-3.0", true)
if acr then
acr:NotifyChange("SlackHacks")
end
prn("profile and addon state have been reset to defaults.")
end)
if not ok then
prn("error resetting addon (" .. tostring(err) .. "). Reloading UI...")
ReloadUI()
end
end
function hookResetProfileOption()
if not (options and options.args and options.args.profiles and options.args.profiles.args) then
return
end
local resetOpt = options.args.profiles.args.reset
if resetOpt then
resetOpt.func = function()
promptResetProfile()
end
resetOpt.confirm = nil
end
end
-- Resets the current profile back to defaults (leaves other profiles/characters alone, same as the
-- native AceDBOptions "Reset Profile" button) and informs the user via a popup.
function resetConfig(reason)
log("Resetting SlackHacks config: " .. tostring(reason))
if Self.db then
Self.db:ResetProfile()
Self.db.global.configVersion = CONFIG_VERSION
Self.db.global.addonVersion = getAddonVersion()
end
notifyConfigReset()
end
function migrateConfig()
local version = Self.db.global.configVersion or 0
if version == CONFIG_VERSION then
return
end
if version > CONFIG_VERSION then
-- Saved data is newer than this build of the addon (e.g. a downgrade/rollback) and can't be trusted.
resetConfig("saved config version " .. version .. " is newer than CONFIG_VERSION " .. CONFIG_VERSION)
return
end
local ok, err = pcall(function()
while version < CONFIG_VERSION do
version = version + 1
local migrate = CONFIG_MIGRATIONS[version]
if migrate then
migrate()
elseif version > 1 then
error("no migration function defined for config version " .. version)
end
Self.db.global.configVersion = version
end
-- Purge all previous errors during a config migration
if Self.db.global and Self.db.global.logs and Self.db.global.logs.error then
wipe(Self.db.global.logs.error)
end
if updateMinimapIcon then
updateMinimapIcon()
elseif Self.UpdateMinimapIcon then
Self.UpdateMinimapIcon()
end
end)
if not ok then
resetConfig(err)
end
end
-- Checks if the addon version has changed since the last load.
-- If updated, purges all previous errors and updates the stored addon version for maintenance.
function checkAddonVersionUpdate()
if not Self.db or not Self.db.global then return end
local currentVersion = getAddonVersion()
local lastVersion = Self.db.global.addonVersion
if lastVersion ~= currentVersion then
if Self.db.global.logs and Self.db.global.logs.error then
wipe(Self.db.global.logs.error)
end
if updateMinimapIcon then
updateMinimapIcon()
elseif Self.UpdateMinimapIcon then
Self.UpdateMinimapIcon()
end
end
Self.db.global.addonVersion = currentVersion
end
--Event Handlers
function Self:OnInitialize()
-- true = share one "Default" profile across all characters instead of a per-character profile
local ok, err = pcall(function()
Self.db = LibStub("AceDB-3.0"):New("SlackHacksDB", dbDefaults, true)
end)
if not ok or not Self.db then
-- Saved variables were corrupted/unreadable: wipe and recreate from defaults rather than erroring out.
prn("failed to load saved settings (" .. tostring(err) .. "), resetting to defaults.")
_G.SlackHacksDB = nil
Self.db = LibStub("AceDB-3.0"):New("SlackHacksDB", dbDefaults, true)
notifyConfigReset()
end
-- One-time nudge for anyone still parked on an old auto-generated per-character profile (e.g. from
-- before "Reset All Data" explicitly forced "Default", or a stale account predating this convention).
local autoCharacterProfile = UnitName("player") .. " - " .. GetRealmName()
if Self.db:GetCurrentProfile() == autoCharacterProfile then
Self.db:SetProfile("Default")
end
registerOptions()
Self:RegisterChatCommand("slack", handleSlashCommand)
Self:RegisterChatCommand("slackhacks", handleSlashCommand)
-- Disabling ActionCam warning/confirmation popup: https://github.com/mpstark/DynamicCam/blob/master/Core.lua#L628C1-L629C68
-- As of a recent client update, this event is no longer dispatched to UIParent (or any other
-- addon-visible frame) -- it's consumed internally by Blizzard_Game's own event dispatcher
-- (Interface/AddOns/Blizzard_Game/Shared/EventRouting.lua), so UnregisterEvent on UIParent is a no-op.
-- `GameEvent` is the (non-local, addon-accessible) table that owns that dispatcher.
if GameEvent and GameEvent.UnregisterInternalEvent then
GameEvent.UnregisterInternalEvent("EXPERIMENTAL_CVAR_CONFIRMATION_NEEDED")
else
UIParent:UnregisterEvent("EXPERIMENTAL_CVAR_CONFIRMATION_NEEDED")
end
end
function isInitialized()
-- Check for presence of last thing loaded at init
return not not Self["configDialog"]
end