Files
MetaHunt/api/savedvariables.lua
DuvelCorp 060fc555fe Unify SavedVariables into two clean roots with one-time migration
Collapse all legacy/scattered SavedVariables from absorbed standalone
addons (FeedOMatic, ZHunter/zButtons, SmartAmmo, AutoQuest, AntiDaze,
AutoStrip, MinimapButton) into two persisted globals with clean per-module
nesting under .modules.<id>.

Migration engine (api/savedvariables.lua):
- MTH_SV_EnsureSchema() with schemaVersion gate, idempotent + non-destructive
- Per-module migrators map old locations -> canonical nested stores
- Driven by an ADDON_LOADED("MetaHunt") handler so it runs only AFTER WoW
  loads the real SavedVariables (fixes announce-every-session bug that ran
  the migration on empty pre-load defaults)
- One-time player-facing chat announce, gated on real legacy data present

Framework/config:
- core-framework InitSavedVariables no longer runs migration at file-load;
  unconditionally strips account-root module aliases
- config.lua stops re-creating root[module] aliases (was duplicating data
  onto disk); canonical store stays under .modules.<id>

Modules:
- feedomatic: bind FOM_* globals as runtime aliases into nested store,
  drop persisted duplicates and .legacy blob
- zhunter: ZHunterMod_Saved re-pointed at .modules.zhunter
- smartammo/autoquest/antidaze/autostrip/minimapbutton read nested store

TOC trimmed to MTH_SavedVariables + MTH_CharSavedVariables only.
Profiles unchanged (already snapshot/apply the nested .modules structure).

Also includes pet-scan refinements: canonical pet key derivation and
level-independent pet signature (schema v3).
2026-09-09 22:52:48 +02:00

435 lines
16 KiB
Lua

------------------------------------------------------
-- MetaHunt — Unified SavedVariables schema + migration engine
------------------------------------------------------
-- PURPOSE
-- Collapse every legacy / scattered SavedVariable into just TWO persisted
-- globals and one consistent shape, so old-addon names (FOM_*, ZHunterMod_Saved,
-- MTHSmartAmmo) and ad-hoc key styles disappear.
--
-- TARGET SHAPE (canonical)
-- MTH_SavedVariables (account-wide) = {
-- schemaVersion = <MTH_SV_SCHEMA_VERSION>,
-- modules = {
-- <id> = { schemaVersion = N, settings = { ...camelCase... }, <dataTables> },
-- feedomatic = { settings{...}, foodQuality, cooking, questFood,
-- addedFoods, removedFoods, localeInfo },
-- },
-- messages = {...}, moduleStates = {...}, profiles = {...},
-- charSnapshots = {...}, versionCheck = {...},
-- }
-- MTH_CharSavedVariables (per-character) = {
-- schemaVersion = <MTH_SV_SCHEMA_VERSION>,
-- modules = {
-- <id> = { schemaVersion = N, settings{...}, <dataTables> },
-- zhunter = { buttons{...}, bar{...}, widgetSpawnLayout },
-- antidaze = { settings{ enabled } },
-- autostrip = { settings{ enabled, display }, frame{ point, relativePoint, x, y } },
-- minimapbutton = { settings{ angle } },
-- },
-- moduleStates = {...},
-- -- pet-system core stays at root (out of scope, heavily referenced):
-- MTH_Pets, feedTracking, petTraining, trainScan, stableScan, petSpellScan,
-- }
--
-- RULES
-- * One-shot, gated by schemaVersion on each root. Idempotent. Non-destructive
-- (data is COPIED into the new location before the old location is cleared).
-- * Lua 5.0-safe (no '#', no string.match/gmatch; pairs()/ipairs() only).
-- * Testable offline: MTH_SV_EnsureSchema(account, char) accepts explicit roots
-- (defaults to the live globals) and returns a report table.
------------------------------------------------------
MTH_SV_SCHEMA_VERSION = 1
------------------------------------------------------
-- Helpers
------------------------------------------------------
local function MTH_SV_IsTable(v)
return type(v) == "table"
end
local function MTH_SV_DeepCopy(orig)
if type(orig) ~= "table" then
return orig
end
local copy = {}
for k, v in pairs(orig) do
copy[MTH_SV_DeepCopy(k)] = MTH_SV_DeepCopy(v)
end
return copy
end
-- Ensure parent[key] is a table and return it.
local function MTH_SV_Ensure(parent, key)
if type(parent[key]) ~= "table" then
parent[key] = {}
end
return parent[key]
end
-- Pick the first non-nil value from a list of candidates; else default.
local function MTH_SV_Coalesce(candidates, default)
for _, v in ipairs(candidates) do
if v ~= nil then
return v
end
end
return default
end
-- Copy every key from src into dst, applying a rename map (oldKey -> newKey).
-- Keys not present in the map keep their original name. Existing dst values win
-- (so re-running never clobbers already-migrated data). Returns count copied.
local function MTH_SV_CopyRenamed(src, dst, renameMap)
local moved = 0
if not MTH_SV_IsTable(src) then
return moved
end
for oldKey, value in pairs(src) do
local newKey = (renameMap and renameMap[oldKey]) or oldKey
if dst[newKey] == nil then
dst[newKey] = value
moved = moved + 1
end
end
return moved
end
-- Count entries in a table (Lua 5.0-safe).
local function MTH_SV_Count(t)
local n = 0
if MTH_SV_IsTable(t) then
for _ in pairs(t) do
n = n + 1
end
end
return n
end
------------------------------------------------------
-- Per-module migrators
-- Each takes the relevant module store table (already ensured) plus context,
-- transforms in place, and records notes into the shared report.
------------------------------------------------------
-- SMARTAMMO (per-character)
-- OLD: modules.smartammo = { enabled, smartEnabled, reloadEnabled,
-- weaponSwapEnabled, legacy = { MTHSmartAmmo = {
-- enabled, reload, weaponSwap } } }
-- + optional global MTHSmartAmmo = { enabled, reload, weaponSwap }
-- NEW: modules.smartammo = { schemaVersion, settings = {
-- smartEnabled, reloadEnabled, weaponSwapEnabled } }
local function MTH_SV_Migrate_SmartAmmo(store, globalMTHSmartAmmo, report)
if not MTH_SV_IsTable(store) then
return
end
local legacy = MTH_SV_IsTable(store.legacy) and store.legacy.MTHSmartAmmo or nil
local g = MTH_SV_IsTable(globalMTHSmartAmmo) and globalMTHSmartAmmo or nil
local settings = MTH_SV_Ensure(store, "settings")
-- smartEnabled: modern flat -> settings -> legacy.enabled -> global.enabled -> old `enabled` -> true
if settings.smartEnabled == nil then
settings.smartEnabled = MTH_SV_Coalesce({
store.smartEnabled,
legacy and legacy.enabled,
g and g.enabled,
store.enabled,
}, true) and true or false
end
if settings.reloadEnabled == nil then
settings.reloadEnabled = MTH_SV_Coalesce({
store.reloadEnabled,
legacy and legacy.reload,
g and g.reload,
}, true) and true or false
end
if settings.weaponSwapEnabled == nil then
settings.weaponSwapEnabled = MTH_SV_Coalesce({
store.weaponSwapEnabled,
legacy and legacy.weaponSwap,
g and g.weaponSwap,
}, true) and true or false
end
-- Clear the old flat/legacy shape now that settings holds the truth.
store.legacy = nil
store.enabled = nil
store.smartEnabled = nil
store.reloadEnabled = nil
store.weaponSwapEnabled = nil
store.schemaVersion = 1
report.smartammo = "settings{smartEnabled=" .. tostring(settings.smartEnabled)
.. ",reloadEnabled=" .. tostring(settings.reloadEnabled)
.. ",weaponSwapEnabled=" .. tostring(settings.weaponSwapEnabled) .. "}"
end
-- AUTOQUEST (per-character)
-- OLD (canonical): MTH_CharSavedVariables.autoquest = { scorpokDrazial,
-- arrowsForSissies, scorpokTooltip, _migrated } mirrored into
-- modules.autoquest = { scorpokDrazial, arrowsForSissies, scorpokTooltip, enabled }
-- plus oldest legacy MTH_CharSavedVariables.questautomation.
-- NEW: modules.autoquest = { schemaVersion, settings = {
-- scorpokDrazial, arrowsForSissies, scorpokTooltip } }
local function MTH_SV_Migrate_AutoQuest(char, report)
local modules = MTH_SV_Ensure(char, "modules")
local store = MTH_SV_Ensure(modules, "autoquest")
local settings = MTH_SV_Ensure(store, "settings")
local root = MTH_SV_IsTable(char.autoquest) and char.autoquest or {}
local qa = MTH_SV_IsTable(char.questautomation) and char.questautomation or {}
local keys = { "scorpokDrazial", "arrowsForSissies", "scorpokTooltip" }
for _, k in ipairs(keys) do
if settings[k] == nil then
settings[k] = MTH_SV_Coalesce({
store[k], -- flat mirror on modules.autoquest
root[k], -- former canonical root .autoquest
qa[k], -- oldest legacy .questautomation
}, false) and true or false
end
end
-- Clear old / duplicate locations.
store.scorpokDrazial = nil
store.arrowsForSissies = nil
store.scorpokTooltip = nil
store._migrated = nil
char.autoquest = nil
char.questautomation = nil
store.schemaVersion = 1
report.autoquest = "settings{scorpokDrazial=" .. tostring(settings.scorpokDrazial)
.. ",arrowsForSissies=" .. tostring(settings.arrowsForSissies)
.. ",scorpokTooltip=" .. tostring(settings.scorpokTooltip) .. "}"
end
-- ANTIDAZE (per-character): char-root .antiDaze{enabled} -> modules.antidaze.settings{enabled}
local function MTH_SV_Migrate_AntiDaze(char, report)
local modules = MTH_SV_Ensure(char, "modules")
local store = MTH_SV_Ensure(modules, "antidaze")
local settings = MTH_SV_Ensure(store, "settings")
local old = MTH_SV_IsTable(char.antiDaze) and char.antiDaze or {}
if settings.enabled == nil and old.enabled ~= nil then
settings.enabled = old.enabled and true or false
end
char.antiDaze = nil
store.schemaVersion = 1
report.antidaze = "settings{enabled=" .. tostring(settings.enabled) .. "}"
end
-- AUTOSTRIP (per-character): char-root .autoStrip{autostrip,display,point,relativePoint,x,y}
-- -> modules.autostrip{autostrip,display,point,relativePoint,x,y}
-- (keys preserved verbatim; only the storage location moves under .modules)
local function MTH_SV_Migrate_AutoStrip(char, report)
local modules = MTH_SV_Ensure(char, "modules")
local store = MTH_SV_Ensure(modules, "autostrip")
local old = MTH_SV_IsTable(char.autoStrip) and char.autoStrip or {}
local carry = { "autostrip", "display", "point", "relativePoint", "x", "y" }
for _, k in ipairs(carry) do
if store[k] == nil and old[k] ~= nil then
store[k] = old[k]
end
end
char.autoStrip = nil
store.schemaVersion = 1
report.autostrip = "store{autostrip=" .. tostring(store.autostrip)
.. ",display=" .. tostring(store.display) .. "}"
end
-- MINIMAPBUTTON (per-character): char-root .minimapButton{angle} -> modules.minimapbutton{angle}
local function MTH_SV_Migrate_MinimapButton(char, report)
local modules = MTH_SV_Ensure(char, "modules")
local store = MTH_SV_Ensure(modules, "minimapbutton")
local old = MTH_SV_IsTable(char.minimapButton) and char.minimapButton or {}
if store.angle == nil and old.angle ~= nil then
store.angle = old.angle
end
char.minimapButton = nil
store.schemaVersion = 1
report.minimapbutton = "store{angle=" .. tostring(store.angle) .. "}"
end
-- FEEDOMATIC (account-wide): the 7 old FeedOMatic globals were stored under
-- modules.feedomatic.legacy.FOM_* (with _G.FOM_* bound as runtime aliases) and
-- duplicated at the account-root alias MTH_SavedVariables.feedomatic.
-- NEW: modules.feedomatic = { schemaVersion, settings, foodQuality, addedFoods,
-- removedFoods, cooking, questFood, localeInfo } — no .legacy wrapper, no root
-- alias. The _G.FOM_* globals stay as RUNTIME aliases (re-bound to these nested
-- tables by MTH_FeedOMatic_SyncSavedVariables) so FeedOMatic.lua is untouched.
local MTH_SV_FOM_MAP = {
{ global = "FOM_Config", key = "settings" },
{ global = "FOM_FoodQuality", key = "foodQuality" },
{ global = "FOM_AddedFoods", key = "addedFoods" },
{ global = "FOM_RemovedFoods", key = "removedFoods" },
{ global = "FOM_Cooking", key = "cooking" },
{ global = "FOM_QuestFood", key = "questFood" },
{ global = "FOM_LocaleInfo", key = "localeInfo" },
}
local function MTH_SV_Migrate_FeedOMatic(account, report)
local modules = MTH_SV_Ensure(account, "modules")
local store = MTH_SV_Ensure(modules, "feedomatic")
local legacy = MTH_SV_IsTable(store.legacy) and store.legacy or {}
for _, m in ipairs(MTH_SV_FOM_MAP) do
if store[m.key] == nil then
local src = legacy[m.global]
if type(src) ~= "table" and _G then
src = _G[m.global]
end
if type(src) == "table" then
store[m.key] = src
end
end
end
store.legacy = nil
account.feedomatic = nil -- drop the account-root alias duplicate
store.schemaVersion = 1
report.feedomatic = "keys{settings,foodQuality,addedFoods,removedFoods,cooking,questFood,localeInfo}"
end
-- ZHUNTER (per-character): the zButton layout used to live in the TOC-declared
-- global ZHunterMod_Saved AND was mirrored into modules.zhunter (two on-disk
-- copies). NEW: modules.zhunter is the single canonical store (button tables,
-- _zbar, enabled, _mth_widget_spawn_layout_v1). ZHunterMod_Saved becomes a pure
-- RUNTIME alias re-bound by MTH_ZH_GetSavedRoot, and is dropped from the TOC.
-- The vestigial account-root alias MTH_SavedVariables.zhunter is removed too.
local function MTH_SV_Migrate_ZHunter(account, char, report)
local modules = MTH_SV_Ensure(char, "modules")
local store = MTH_SV_Ensure(modules, "zhunter")
-- If the nested store is still empty but a per-char global carries data
-- (legacy TOC copy present during the transition), adopt it once.
if _G and MTH_SV_IsTable(_G.ZHunterMod_Saved) and _G.ZHunterMod_Saved ~= store then
if MTH_SV_Count(store) == 0 then
for k, v in pairs(_G.ZHunterMod_Saved) do
store[k] = v
end
end
end
account.zhunter = nil -- drop the vestigial account-root alias duplicate
report.zhunter = "modules.zhunter canonical; ZHunterMod_Saved -> runtime alias"
end
------------------------------------------------------
-- Orchestrator
------------------------------------------------------
-- MTH_SV_EnsureSchema(account, char)
-- account / char default to the live globals. Returns a report table.
-- Gated by schemaVersion on each root so it only runs once; idempotent anyway.
function MTH_SV_EnsureSchema(account, char)
if account == nil then account = MTH_SavedVariables end
if char == nil then char = MTH_CharSavedVariables end
local report = { ran = false }
if not MTH_SV_IsTable(account) or not MTH_SV_IsTable(char) then
report.error = "roots unavailable"
return report
end
local accountDone = (tonumber(account.schemaVersion) or 0) >= MTH_SV_SCHEMA_VERSION
local charDone = (tonumber(char.schemaVersion) or 0) >= MTH_SV_SCHEMA_VERSION
if accountDone and charDone then
report.skipped = true
return report
end
report.ran = true
local accountModules = MTH_SV_Ensure(account, "modules")
local charModules = MTH_SV_Ensure(char, "modules")
-- Detect whether there is genuine legacy data to relocate BEFORE the migrators
-- consume/delete it. On a brand-new install there is nothing to move, so we still
-- stamp the schema (to avoid re-checking) but skip the player-facing announcement.
local smartLegacy = MTH_SV_IsTable(charModules.smartammo) and charModules.smartammo.legacy or nil
local feedLegacy = MTH_SV_IsTable(accountModules.feedomatic) and accountModules.feedomatic.legacy or nil
report.changed = (account.feedomatic ~= nil)
or (account.zhunter ~= nil)
or (char.antiDaze ~= nil)
or (char.autoStrip ~= nil)
or (char.minimapButton ~= nil)
or (char.autoquest ~= nil)
or (char.questautomation ~= nil)
or (feedLegacy ~= nil)
or (smartLegacy ~= nil)
or (_G ~= nil and (_G.FOM_Config ~= nil or _G.ZHunterMod_Saved ~= nil or _G.MTHSmartAmmo ~= nil))
or false
-- ---- account-wide modules ----
MTH_SV_Migrate_FeedOMatic(account, report)
-- ---- per-character modules ----
MTH_SV_Migrate_SmartAmmo(
MTH_SV_Ensure(charModules, "smartammo"),
_G and _G.MTHSmartAmmo or nil,
report
)
if _G then _G.MTHSmartAmmo = nil end
MTH_SV_Migrate_AutoQuest(char, report)
MTH_SV_Migrate_AntiDaze(char, report)
MTH_SV_Migrate_AutoStrip(char, report)
MTH_SV_Migrate_MinimapButton(char, report)
MTH_SV_Migrate_ZHunter(account, char, report)
-- ---- stamp ----
account.schemaVersion = MTH_SV_SCHEMA_VERSION
char.schemaVersion = MTH_SV_SCHEMA_VERSION
-- ---- player-facing announcement (only when real legacy data was relocated) ----
-- Guarded on MTH_Log so the offline test harness (no MetaHunt runtime) stays silent.
if report.changed and type(MTH_Log) == "function" then
local order = {
"feedomatic", "smartammo", "autoquest",
"antidaze", "autostrip", "minimapbutton", "zhunter",
}
local done = {}
for _, id in ipairs(order) do
if report[id] ~= nil then
table.insert(done, id)
end
end
MTH_Log("Tidying your saved settings into the new 2.0 layout (one-time)...")
if table.getn(done) > 0 then
MTH_Log(" consolidated: " .. table.concat(done, ", "))
end
MTH_Log("Saved-settings cleanup complete. Your options were carried over unchanged.")
end
return report
end
------------------------------------------------------
-- Load trigger
------------------------------------------------------
-- Run the migration exactly once, AFTER WoW has loaded MetaHunt's SavedVariables
-- from disk. ADDON_LOADED with arg1 == "MetaHunt" is the earliest point at which the
-- real account + per-character saved data is actually available. Running it any earlier
-- (e.g. from a top-level MTH:InitSavedVariables() at file-load) operates on the empty
-- default tables WoW then overwrites when it loads the SV file — which is exactly why
-- the migration used to re-announce every login and /reload. CreateFrame is guarded so
-- the offline test harness (no WoW API) simply skips this.
local MTH_SV_LoadFrame = CreateFrame and CreateFrame("Frame")
if MTH_SV_LoadFrame then
MTH_SV_LoadFrame:RegisterEvent("ADDON_LOADED")
MTH_SV_LoadFrame:SetScript("OnEvent", function()
if event ~= "ADDON_LOADED" or arg1 ~= "MetaHunt" then
return
end
MTH_SV_LoadFrame:UnregisterEvent("ADDON_LOADED")
if MTH and MTH.InitSavedVariables then
MTH:InitSavedVariables()
end
if type(MTH_SV_EnsureSchema) == "function" then
MTH_SV_EnsureSchema(MTH_SavedVariables, MTH_CharSavedVariables)
end
end)
end