-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProfileMigrator.lua
More file actions
398 lines (351 loc) · 11.2 KB
/
ProfileMigrator.lua
File metadata and controls
398 lines (351 loc) · 11.2 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
--- ProfileMigrator.lua
-- Handles profile schema migrations and backwards compatibility
-- Ensures user profiles remain valid across addon updates with schema changes
local AceAddon = LibStub("AceAddon-3.0")
local addon = AceAddon:GetAddon("SimpleUnitFrames", true)
-- Ensure addon is available; fail gracefully if load order is early.
if not addon then
return
end
-- Migration registry: maps from version → migration function
-- Each migration function receives (addon, profile, fromVersion) and modifies profile in-place
local migrations = {}
-- Current profile schema version - increment when making breaking changes
local CURRENT_SCHEMA_VERSION = 3
--- Register a profile migration for a specific schema change
-- @param fromVersion number - Migrate from this version
-- @param toVersion number - Migrate to this version
-- @param migrationFunc function(addon, profile, fromVersion) - Performs the migration
function addon:RegisterProfileMigration(fromVersion, toVersion, migrationFunc)
if not migrations[fromVersion] then
migrations[fromVersion] = {}
end
migrations[fromVersion][toVersion] = migrationFunc
-- Note: Can't log here because db doesn't exist yet during module load
end
--- Run all necessary migrations to bring profile from old version to current
-- @param profile table - Profile to migrate
-- @param fromVersion number - Starting version
-- @return boolean - True if migration successful, false if rolled back to defaults
function addon:RunProfileMigrations(profile, fromVersion)
if not fromVersion then
fromVersion = 0
end
if fromVersion == CURRENT_SCHEMA_VERSION then
-- Already current, no migration needed
return true
end
if fromVersion > CURRENT_SCHEMA_VERSION then
-- Profile is from a newer version - might be downgrading
self:DebugLog("ProfileMigration", string.format(
"Profile version %d is newer than addon schema %d (possible downgrade)",
fromVersion, CURRENT_SCHEMA_VERSION
), 1)
-- Safe approach: reset to current, user can re-configure
return false
end
-- Run migration chain from fromVersion to CURRENT_SCHEMA_VERSION
local currentVersion = fromVersion
while currentVersion < CURRENT_SCHEMA_VERSION do
local nextVersion = currentVersion + 1
if not migrations[currentVersion] or not migrations[currentVersion][nextVersion] then
-- No migration path defined - skip this step
self:DebugLog("ProfileMigration", string.format(
"No migration path from v%d to v%d, skipping",
currentVersion, nextVersion
), 2)
currentVersion = nextVersion
else
-- Execute migration
local migrationFunc = migrations[currentVersion][nextVersion]
local success, err = pcall(migrationFunc, self, profile, currentVersion)
if not success then
self:DebugLog("ProfileMigration", string.format(
"Migration v%d -> v%d failed: %s",
currentVersion, nextVersion, tostring(err)
), 1)
-- Migration failed - rollback to defaults
return false
end
self:DebugLog("ProfileMigration", string.format(
"Migrated profile from v%d to v%d",
currentVersion, nextVersion
), 2)
currentVersion = nextVersion
end
end
-- Set final schema version
profile._schemaVersion = CURRENT_SCHEMA_VERSION
self:DebugLog("ProfileMigration", string.format(
"Profile migration complete: now at schema v%d",
CURRENT_SCHEMA_VERSION
), 2)
return true
end
--- Ensure profile has all required top-level tables
-- Safely adds missing tables without overwriting existing data
-- @param profile table - Profile to validate
-- @param defaults table - Defaults table to reference
function addon:EnsureProfileDefaults(profile, defaults)
if not profile then
return false
end
-- Ensure units table and all unit types exist
if not profile.units then
profile.units = {}
end
for unitType, unitDefaults in pairs(defaults.profile.units or {}) do
if not profile.units[unitType] then
profile.units[unitType] = CopyTableDeep(unitDefaults)
end
end
-- Ensure media table exists
if not profile.media then
profile.media = {}
end
for key, value in pairs(defaults.profile.media or {}) do
if profile.media[key] == nil then
profile.media[key] = value
end
end
-- Ensure optionsUI table exists
if not profile.optionsUI then
profile.optionsUI = {}
end
for key, value in pairs(defaults.profile.optionsUI or {}) do
if profile.optionsUI[key] == nil then
profile.optionsUI[key] = value
end
end
-- Ensure other tables exist
if not profile.sizes then
profile.sizes = {}
end
if not profile.movers then
profile.movers = {}
end
if not profile.castbar then
profile.castbar = {}
end
if not profile.plugins then
profile.plugins = {}
end
if not profile.performance then
profile.performance = {}
end
if not profile.indicators then
profile.indicators = {}
end
if not profile.minimap then
profile.minimap = {}
end
if not profile.enhancements then
profile.enhancements = {}
end
if not profile.debug then
profile.debug = {}
end
if not profile.customTrackers then
profile.customTrackers = {}
end
if not profile.party then
profile.party = {}
end
return true
end
--- Check profile integrity and repair if necessary
-- @param profile table - Profile to validate
-- @param defaults table - Defaults to validate against
-- @return boolean - True if profile valid/repaired, false if rollback needed
function addon:ValidateProfileIntegrity(profile, defaults)
if not profile then
return false
end
local issues = {}
-- Check for obvious data corruption
if type(profile.units) ~= "table" then
table.insert(issues, "units table corrupted")
profile.units = {}
end
if type(profile.media) ~= "table" then
table.insert(issues, "media table corrupted")
profile.media = {}
end
if type(profile.movers) ~= "table" then
table.insert(issues, "movers table corrupted")
profile.movers = {}
end
-- Check that each unit type has required structure
for unitType, unitConfig in pairs(profile.units or {}) do
if type(unitConfig) ~= "table" then
table.insert(issues, string.format("unit config for %s corrupted", unitType))
profile.units[unitType] = CopyTableDeep(defaults.profile.units[unitType] or {})
end
end
-- Log any issues found
if #issues > 0 then
self:DebugLog("ProfileMigration", string.format(
"Profile integrity issues detected: %s (auto-repaired)",
table.concat(issues, ", ")
), 2)
end
return true
end
--- Initialize profile migration system and check for needed migrations
-- Called during OnInitialize after profile is loaded
function addon:InitializeProfileMigration()
-- Get current profile's schema version (default to 0 for legacy profiles)
local schemaVersion = self.db.profile._schemaVersion or 0
-- Run migrations if needed
if schemaVersion < CURRENT_SCHEMA_VERSION then
self:DebugLog("ProfileMigration", string.format(
"Profile schema out of date (v%d, expected v%d), running migrations...",
schemaVersion, CURRENT_SCHEMA_VERSION
), 2)
-- Attempt migration
local migrationSuccess = self:RunProfileMigrations(self.db.profile, schemaVersion)
if not migrationSuccess then
self:DebugLog("ProfileMigration",
"Profile migration failed or profile too old. Restoring defaults while preserving movers.",
1)
-- Preserve movers from old profile (user's window positions)
local savedMovers = CopyTableDeep(self.db.profile.movers or {})
-- Reset to defaults
self.db:ResetProfile()
-- Restore movers if they existed
if next(savedMovers) then
self.db.profile.movers = savedMovers
self:DebugLog("ProfileMigration", "Restored saved window positions.", 2)
end
end
end
-- Ensure all required fields exist
self:EnsureProfileDefaults(self.db.profile, self.defaults)
-- Validate profile integrity
self:ValidateProfileIntegrity(self.db.profile, self.defaults)
-- Set schema version to current
self.db.profile._schemaVersion = CURRENT_SCHEMA_VERSION
end
-- Example migration from v0 to v1 (to be used when first schema change is made)
-- Uncomment and customize when needed:
--[[
addon:RegisterProfileMigration(0, 1, function(addon, profile, fromVersion)
-- Example: profile.newField = profile.newField or defaultValue
addon:DebugLog("ProfileMigration", "Applied v0->v1 migration: added new fields", 2)
end)
--]]
-- Migration v1 -> v2: Add castbar configuration for all units that support castbars
addon:RegisterProfileMigration(1, 2, function(addon, profile, fromVersion)
local updated = false
-- Add player castbar config if missing (uses BELOW_CLASSPOWER anchor)
if profile.units and profile.units.player then
if not profile.units.player.castbar then
profile.units.player.castbar = {
enabled = true,
anchor = "BELOW_CLASSPOWER",
gap = 8,
offsetY = 0,
}
updated = true
end
end
-- Add target castbar config if missing
if profile.units and profile.units.target then
if not profile.units.target.castbar then
profile.units.target.castbar = {
enabled = true,
anchor = "BELOW_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
-- Add party castbar config if missing
if profile.units and profile.units.party then
if not profile.units.party.castbar then
profile.units.party.castbar = {
enabled = true,
anchor = "ABOVE_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
-- Add raid castbar config if missing
if profile.units and profile.units.raid then
if not profile.units.raid.castbar then
profile.units.raid.castbar = {
enabled = true,
anchor = "ABOVE_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
-- Add tot castbar config if missing
if profile.units and profile.units.tot then
if not profile.units.tot.castbar then
profile.units.tot.castbar = {
enabled = true,
anchor = "BELOW_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
-- Add focustarget castbar config if missing
if profile.units and profile.units.focustarget then
if not profile.units.focustarget.castbar then
profile.units.focustarget.castbar = {
enabled = true,
anchor = "BELOW_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
if updated then
addon:DebugLog("ProfileMigration", "Applied v1->v2 migration: added castbar configuration for all castbar-capable units", 2)
end
end)
-- Migration v2 -> v3: Add player and target castbar configuration (for profiles that already had v2 from earlier party/raid migration)
addon:RegisterProfileMigration(2, 3, function(addon, profile, fromVersion)
local updated = false
-- Add player castbar config if missing (uses BELOW_CLASSPOWER anchor)
if profile.units and profile.units.player then
if not profile.units.player.castbar then
profile.units.player.castbar = {
enabled = true,
anchor = "BELOW_CLASSPOWER",
gap = 8,
offsetY = 0,
}
updated = true
end
end
-- Add target castbar config if missing
if profile.units and profile.units.target then
if not profile.units.target.castbar then
profile.units.target.castbar = {
enabled = true,
anchor = "BELOW_FRAME",
gap = 2,
offsetY = 0,
}
updated = true
end
end
if updated then
addon:DebugLog("ProfileMigration", "Applied v2->v3 migration: added player/target castbar configuration", 2)
end
end)
-- Export for testing
addon.ProfileMigrator = {
GetSchemaVersion = function() return CURRENT_SCHEMA_VERSION end,
GetRegisteredMigrations = function() return migrations end,
}