diff --git a/CHANGELOG.md b/CHANGELOG.md index e0daa37..8481e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to MetaHunt will be documented in this file. +## [2.0.0] - Unreleased + +### Changed + +- **Settings storage tidied into one clean layout**: The settings MetaHunt inherited from the older stand-alone addons it now replaces (Feed-O-Matic, ZHunter/zButtons, SmartAmmo, and the quest/anti-daze/auto-strip/minimap helpers) used to be scattered across many separate saved entries, several of them duplicated on disk. MetaHunt now keeps everything in one organised place, grouped per module. The clean-up runs automatically and once, the first time you log in after updating: all of your existing settings and layouts are carried over unchanged — nothing is reset — and the old duplicate copies are removed so your saved-variables file stays small and consistent. + +### Fixed + +- **Pets — duplicate and phantom entries in your pet collection**: MetaHunt could record the same tamed pet more than once — a fresh copy appeared after you logged out and back in, or after your pet leveled up — and a few very early pets you had long since dismissed lingered as though they were still with you. MetaHunt now recognises each pet reliably from one session to the next (and as it levels), automatically merges the duplicate entries it had already created while keeping the correct taming details (where, and at what level, you tamed each one), and tidies the leftover phantom pets away into your pet history. Your full taming history is preserved — nothing is deleted. + + ## [1.5.2] - 2026-09-08 Res on Octowow server. diff --git a/MetaHunt.toc b/MetaHunt.toc index d26b3c4..50a0b74 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -3,8 +3,8 @@ ## Title: MetaHunt - |cff00ff00Hunter ## Author: Metasploit and his Copilot ;) ## Notes: Unified addon suite for huntards making old addons compatible with Twow, and adding an arsenal of never seen Hunter's utilities. -## SavedVariables: MTH_SavedVariables, FOM_Config, FOM_FoodQuality, FOM_AddedFoods, FOM_RemovedFoods, FOM_Cooking, FOM_QuestFood, FOM_LocaleInfo -## SavedVariablesPerCharacter: ZHunterMod_Saved, MTH_CharSavedVariables +## SavedVariables: MTH_SavedVariables +## SavedVariablesPerCharacter: MTH_CharSavedVariables data\init.lua diff --git a/README.MD b/README.MD index f6f7024..e916721 100644 --- a/README.MD +++ b/README.MD @@ -1,14 +1,15 @@ # MetaHunt MetaHunt is a Twow Hunter toolkit. -It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on Twow. +It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on twow. It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider. ### Important notes + - Altough Nampower isn't mandatory for most of the features, it is a must-have for some. - If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. -- MetaHunt is fully compatible with Quiver and is not a replacement for it. -- Zero support for Capycraft and Ravencraft mafia. +- MetaHunt is fully compatible with QUIVER, and is not a replacement for it. +- MetaHunt don't and will never support Capycraft/Ravencraft (mafia) servers, after the DDOS attacks they conducted on other servers, to only name that. Fuck you. ## Twow Data @@ -23,6 +24,14 @@ MetaHunt ships with large Twow datastores : You can browse all the beasts in game within the Book of Huntards, but also on the github site : https://DuvelCorp.github.io/MetaHunt-Web/ + ## The Great Book of Huntards + + The Book is the main GUI of MetaHunt, it allows to display the data collected about you and your pet, and browse the addon's data to find anything a Hunter needs. + +- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest +- Track all info about the pet abilities that you know and don't know yet +- Track all info about your stabled pets +- Keep an history of all your pets after you abandon them or if they run away. ## Smart Ammo @@ -31,7 +40,7 @@ You can browse all the beasts in game within the Book of Huntards, but also on t - Auto equip the right ammo when you swap from a box/xbow to a gun and the other way around. ## MM Widget - Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. + Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. You can also bind it to turn it into a very efficient one-button rotation. No more excuse to not be amongst top DPS. ## Tooltips @@ -45,11 +54,13 @@ You can browse all the beasts in game within the Book of Huntards, but also on t Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with new functionalities - zAmmo : Track all your ammo Live and swap them in a click or bind -- zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing an intelligent button that always show you the right spell to use depending on the situation +- zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing a main button that always swap on the right spell to use depending on the situation - zTrack: All your trackings in one collapsible bar - zAspect: All your trackings in one collapsible bar - zRanged : All the ranged weapons currently in your bags to swap them fast -- But also zBar, zCrafts, zMounts, zToys and zCompanions. +- But also zCrafts, zMounts, zToys and zCompanions. + +Additionally, a zBar can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. ## Feed-O-Matic @@ -63,26 +74,22 @@ The good old Chronometer addon, purged of non-necessary things and reconfigured Old addon used to display Targets' enhanced information when you click on your minimap tracking things. MetaHunt offers many more customization options for it. + ## Ammo labels + + You can display on all ammo in your bags and/or bank their type and/or dps to instantly see which of those damn same arrows icons really are. ## Auto-Buy -Define your own simple rules for auto buy ammo and pet food when you open a vendor. +Define your own simple rules for auto buy ammo and pet food when you open a vendor. You can also make a food-auto-buy planning based on all your stabled pet's diets. ## Auto-Quest -Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the mobs involved. +Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the Blasted Land mobs involved. ## Beast model Viewer Display the model of all beasts in the world. Always know what you are going to tame exactly before running there ! You can also browse all skin models of the same pet family to find the best-looking pet according to your tastes. - ## The Great Book of Huntards - - The Book is the main GUI of MetaHunt, it allows to display the data constantly collected about you and your pet, and browse the addon's data to find anything a Hunter needs. - -- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest -- Track all info about the pet abilities that you know and don't know yet -- Track all info about your stabled pets -- Keep an history of all your pets after you abandon them or if they run away. + diff --git a/api/AntiDaze.lua b/api/AntiDaze.lua index d20ffba..40079be 100644 --- a/api/AntiDaze.lua +++ b/api/AntiDaze.lua @@ -10,10 +10,23 @@ local function AntiDaze_GetSaved() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.antiDaze) ~= "table" then - MTH_CharSavedVariables.antiDaze = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("antidaze") end - return MTH_CharSavedVariables.antiDaze + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.antidaze) ~= "table" then + MTH_CharSavedVariables.modules.antidaze = {} + end + store = MTH_CharSavedVariables.modules.antidaze + end + if type(store.settings) ~= "table" then + store.settings = {} + end + return store.settings end local function AntiDaze_IsEnabled() diff --git a/api/AutoStrip.lua b/api/AutoStrip.lua index 762e448..b153b28 100644 --- a/api/AutoStrip.lua +++ b/api/AutoStrip.lua @@ -33,10 +33,20 @@ function AutoStrip_GetSaved() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoStrip) ~= "table" then - MTH_CharSavedVariables.autoStrip = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autostrip") end - return MTH_CharSavedVariables.autoStrip + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.autostrip) ~= "table" then + MTH_CharSavedVariables.modules.autostrip = {} + end + store = MTH_CharSavedVariables.modules.autostrip + end + return store end local AUTO_STRIP_ORDER = {16, 17, 18, 5, 7, 1, 3, 10, 8, 6, 9} diff --git a/api/config.lua b/api/config.lua index ecdb00b..772d006 100644 --- a/api/config.lua +++ b/api/config.lua @@ -38,7 +38,10 @@ local function MTH_ConfigEnsureStore(module_name) root.modules[module_name] = legacy end - root[module_name] = root.modules[module_name] + -- NOTE: we intentionally do NOT mirror root[module_name] = root.modules[module_name]. + -- That old alias made WoW serialize a full DUPLICATE of every module store at the + -- account root on disk. The canonical store is root.modules[module_name] only. + root[module_name] = nil MTH_SavedVariables = root return root.modules[module_name] end diff --git a/api/core-framework.lua b/api/core-framework.lua index f7e47c3..cd17a90 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -1187,8 +1187,6 @@ function MTH:InitSavedVariables() if not MTH_SavedVariables then MTH_SavedVariables = { modules = {}, - feedomatic = {}, - zhunter = {}, } end @@ -1198,12 +1196,6 @@ function MTH:InitSavedVariables() if not MTH_SavedVariables.moduleStates then MTH_SavedVariables.moduleStates = {} end - if not MTH_SavedVariables.modules.feedomatic then - MTH_SavedVariables.modules.feedomatic = MTH_SavedVariables.feedomatic or {} - end - if not MTH_SavedVariables.modules.zhunter then - MTH_SavedVariables.modules.zhunter = MTH_SavedVariables.zhunter or {} - end if type(MTH_SavedVariables.messages) ~= "table" then MTH_SavedVariables.messages = {} end @@ -1215,9 +1207,6 @@ function MTH:InitSavedVariables() end end - MTH_SavedVariables.feedomatic = MTH_SavedVariables.modules.feedomatic - MTH_SavedVariables.zhunter = MTH_SavedVariables.modules.zhunter - if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end @@ -1271,6 +1260,24 @@ function MTH:InitSavedVariables() end end end + + -- NOTE: the legacy/scattered SavedVariables consolidation (MTH_SV_EnsureSchema) + -- is intentionally NOT called here. InitSavedVariables also runs at file-load time + -- (before WoW has loaded the SV file from disk), when the roots are still empty + -- defaults. Running the migration there operated on throwaway tables and made it + -- re-announce every session. The migration is now driven from an ADDON_LOADED + -- handler in api/savedvariables.lua, which fires once the real saved data exists. + + -- Account-root module aliases must NEVER persist: the canonical stores live under + -- .modules.. feedomatic/zhunter used to be mirrored here (config.lua alias + + -- InitSavedVariables seeds), which made WoW serialize a full DUPLICATE on disk. + -- Strip them unconditionally on every load — even after the one-shot migration gate + -- has closed — so any stray copy re-created by older code, or already sitting in an + -- on-disk file from a previous version, is removed. + if type(MTH_SavedVariables) == "table" then + MTH_SavedVariables.feedomatic = nil + MTH_SavedVariables.zhunter = nil + end end function MTH:GetModuleSavedVariables(name) diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index 196981e..45b4bf1 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -2,7 +2,7 @@ if not MTH then error("MetaHunt core framework missing: api/core-framework.lua must load before api/core-scan-stablemaster.lua") end -local MTH_PETS_SCHEMA_VERSION = 2 +local MTH_PETS_SCHEMA_VERSION = 3 local MTH_PETS_CORE_HOOK_BOUNDARY_KEY = "core-pet-rename-hook" local MTH_ST_FULL_DEBUG_TRACE = false MTH_PETS_TRACE_CONSISTENCY = false @@ -914,6 +914,21 @@ local function MTH_PETS_ParseCreatureIdFromGuid(guid) return nil end +-- Canonical pet key: the pet GUID with its low 16 bits (last 4 hex chars) zeroed. +-- On this core the low 16 bits change every session while the high portion stays +-- stable per physical pet, so the full GUID cannot be used as a durable identity +-- key. The canonical key is that durable identity, derived purely from the GUID. +function MTH_PETS_CanonicalKey(guid) + if type(guid) ~= "string" or guid == "" then + return nil + end + local body = string.gsub(guid, "^0[xX]", "") + if string.len(body) < 5 then + return nil + end + return "0x" .. string.sub(body, 1, string.len(body) - 4) .. "0000" +end + local function MTH_PETS_ParseBeastLevelBounds(levelField) local levelText = tostring(levelField or "") if levelText == "" then @@ -1252,10 +1267,11 @@ local function MTH_PETS_ResolveConfirmedTameBeastId(source, snapshot) end local function MTH_PETS_MakeSignature(name, family, level) + -- Level intentionally excluded: it mutates on level-up, so baking it into the + -- identity signature made the same pet look like a new one after each level. local cleanName = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(name)) local cleanFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(family)) - local numericLevel = tonumber(level) or 0 - return cleanName .. "|" .. cleanFamily .. "|" .. tostring(numericLevel) + return cleanName .. "|" .. cleanFamily end local function MTH_PETS_RowHasAnyTameMetadata(row) @@ -1546,11 +1562,14 @@ local function MTH_PETS_RepairActiveRowsByGuid(pets) local groupedByGuid = {} for petId, row in pairs(pets.petStore.activeById) do if type(row) == "table" and type(row.guid) == "string" and row.guid ~= "" then - local guid = row.guid - if type(groupedByGuid[guid]) ~= "table" then - groupedByGuid[guid] = {} + -- Group by the CANONICAL key, not the full guid: the full guid is volatile + -- across sessions, so grouping on it never catches cross-session duplicates + -- of the same physical pet (the root cause of the duplicate-row bug). + local key = MTH_PETS_CanonicalKey(row.guid) or row.guid + if type(groupedByGuid[key]) ~= "table" then + groupedByGuid[key] = {} end - table.insert(groupedByGuid[guid], tostring(petId)) + table.insert(groupedByGuid[key], tostring(petId)) end end @@ -1627,6 +1646,9 @@ local function MTH_PETS_EnsurePetStoreSchema(pets) row.tameVerified = true end end + -- Re-sign level-free every load so the signature index can never retain a + -- level-baked key from older data. + row.signature = MTH_PETS_MakeSignature(row.name, row.family) if type(row.signature) == "string" and row.signature ~= "" then if type(petStore.signatureIndex[row.signature]) ~= "table" then petStore.signatureIndex[row.signature] = {} @@ -1634,7 +1656,12 @@ local function MTH_PETS_EnsurePetStoreSchema(pets) table.insert(petStore.signatureIndex[row.signature], resolvedPetId) end if type(row.guid) == "string" and row.guid ~= "" then - petStore.guidIndex[row.guid] = resolvedPetId + petStore.guidIndex[MTH_PETS_CanonicalKey(row.guid) or row.guid] = resolvedPetId + end + -- beastId is mislabeled (per-pet number, not creature id); expose petNumber + -- additively without breaking existing readers. + if row.beastId ~= nil and row.petNumber == nil then + row.petNumber = row.beastId end if row.loyaltyLevel == nil and type(row.loyalty) == "string" then row.loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(row.loyalty) @@ -1800,8 +1827,16 @@ local function MTH_PETS_ApplyCurrentPetFromActiveRow(cp, petId, row) end end +-- Forward declaration: the one-shot orphan cleanup is defined later (it reuses +-- MTH_PETS_MoveToHistory, declared further down), but MTH_PETS_EnsureSchema needs +-- to reference it here. By call time (a load-time event) it is assigned. +local MTH_PETS_ArchiveOrphanActiveRows + local function MTH_PETS_EnsureSchema(pets) local now = time() + -- Capture BEFORE the stamp block rewrites schemaVersion, so we know whether the + -- one-time v3 orphan cleanup still needs to run for this character. + local needsOrphanCleanup = (tonumber(pets.schemaVersion) or 0) < 3 if pets.schemaVersion ~= MTH_PETS_SCHEMA_VERSION then pets.schemaVersion = MTH_PETS_SCHEMA_VERSION pets.schemaMigratedAt = now @@ -1810,7 +1845,7 @@ local function MTH_PETS_EnsureSchema(pets) end pets.schemaMigration.version = MTH_PETS_SCHEMA_VERSION pets.schemaMigration.at = now - pets.schemaMigration.note = "beta canonical store" + pets.schemaMigration.note = "canonical identity keys + orphan cleanup" end if pets.updatedAt == nil then pets.updatedAt = 0 @@ -1839,6 +1874,11 @@ local function MTH_PETS_EnsureSchema(pets) end MTH_PETS_EnsureCurrentPetSchema(pets) MTH_PETS_EnsurePetStoreSchema(pets) + -- After dedup + index rebuild (done inside EnsurePetStoreSchema), sweep legacy + -- orphan rows (active but neither the current pet nor stabled) into history once. + if needsOrphanCleanup and type(MTH_PETS_ArchiveOrphanActiveRows) == "function" then + MTH_PETS_ArchiveOrphanActiveRows(pets, "migration-recovered") + end end local function MTH_PETS_MarkStableVisited(pets, source) @@ -2067,19 +2107,22 @@ local function MTH_PETS_SelectPetIdBySnapshot(store, snapshot, previousPetId) if previousPetId and store.activeById[previousPetId] then local previousRow = store.activeById[previousPetId] - if previousRow.guid and snapshot.guid and previousRow.guid == snapshot.guid then + if previousRow.guid and snapshot.guid + and MTH_PETS_CanonicalKey(previousRow.guid) == MTH_PETS_CanonicalKey(snapshot.guid) then return previousPetId end end - if snapshot.guid and store.guidIndex[snapshot.guid] and store.activeById[store.guidIndex[snapshot.guid]] then - return store.guidIndex[snapshot.guid] + local snapshotKey = snapshot.guid and MTH_PETS_CanonicalKey(snapshot.guid) or nil + if snapshotKey and store.guidIndex[snapshotKey] and store.activeById[store.guidIndex[snapshotKey]] then + return store.guidIndex[snapshotKey] end - if snapshot.guid and snapshot.guid ~= "" then + if snapshotKey then for petId, row in pairs(store.activeById or {}) do - if type(row) == "table" and row.guid == snapshot.guid then - store.guidIndex[snapshot.guid] = tostring(petId) + if type(row) == "table" and type(row.guid) == "string" + and MTH_PETS_CanonicalKey(row.guid) == snapshotKey then + store.guidIndex[snapshotKey] = tostring(petId) return tostring(petId) end end @@ -2240,16 +2283,18 @@ local function MTH_PETS_UpsertActivePetFromSnapshot(pets, snapshot, source, opti petStore.stableSlotIndex[row.stableSlot] = nil end row.stableSlot = nil - if row.guid and row.guid ~= snapshot.guid and petStore.guidIndex[row.guid] == petId then - petStore.guidIndex[row.guid] = nil + local oldGuidKey = row.guid and MTH_PETS_CanonicalKey(row.guid) or nil + local newGuidKey = snapshot.guid and MTH_PETS_CanonicalKey(snapshot.guid) or nil + if oldGuidKey and oldGuidKey ~= newGuidKey and petStore.guidIndex[oldGuidKey] == petId then + petStore.guidIndex[oldGuidKey] = nil end local context = MTH_PETS_CaptureContext() MTH_PETS_ApplySnapshotToRow(row, snapshot, source, context) row.firstSeen = row.firstSeen or time() - if snapshot.guid and snapshot.guid ~= "" then - petStore.guidIndex[snapshot.guid] = petId + if newGuidKey then + petStore.guidIndex[newGuidKey] = petId end if snapshot.signature and snapshot.signature ~= "" then MTH_PETS_AddSignatureIndex(petStore, snapshot.signature, petId) @@ -2573,8 +2618,9 @@ local function MTH_PETS_MoveToHistory(pets, petId, source, context, reason) if row.signature then MTH_PETS_RemoveSignatureIndex(petStore, row.signature, resolvedPetId) end - if row.guid and petStore.guidIndex[row.guid] == resolvedPetId then - petStore.guidIndex[row.guid] = nil + local guidKey = row.guid and MTH_PETS_CanonicalKey(row.guid) or nil + if guidKey and petStore.guidIndex[guidKey] == resolvedPetId then + petStore.guidIndex[guidKey] = nil end if row.stableSlot and petStore.stableSlotIndex[row.stableSlot] == resolvedPetId then petStore.stableSlotIndex[row.stableSlot] = nil @@ -2597,6 +2643,56 @@ local function MTH_PETS_MoveToHistory(pets, petId, source, context, reason) return true end +-- One-shot legacy cleanup (v3 migration): move "orphan" active rows into history. +-- An orphan is an active row that is neither the current pet nor associated with a +-- stable slot in any way. In normal play a pet is always either summoned or stabled, +-- so such rows are abandoned pets that older code failed to archive. They are moved +-- (not deleted): all tame history is preserved. Assigned to the forward-declared +-- local so MTH_PETS_EnsureSchema (defined earlier) can call it. +function MTH_PETS_ArchiveOrphanActiveRows(pets, reason) + local petStore = type(pets) == "table" and pets.petStore or nil + if type(petStore) ~= "table" or type(petStore.activeById) ~= "table" then + return 0 + end + local currentId = tostring(pets.currentPetId or petStore.activeCurrentId or "") + local orphanIds = {} + for petId, row in pairs(petStore.activeById) do + if type(row) == "table" then + local id = tostring(petId) + local isCurrent = (currentId ~= "" and id == currentId) + local inSlotIndex = false + if type(petStore.stableSlotIndex) == "table" then + for _, mappedId in pairs(petStore.stableSlotIndex) do + if tostring(mappedId or "") == id then + inSlotIndex = true + break + end + end + end + local hasStableSlot = tonumber(row.stableSlot) and tonumber(row.stableSlot) > 0 + local hasStableInfo = type(row.stableInfo) == "table" + if not isCurrent and not inSlotIndex and not hasStableSlot and not hasStableInfo then + table.insert(orphanIds, id) + end + end + end + local archived = 0 + for i = 1, table.getn(orphanIds) do + local id = orphanIds[i] + local row = petStore.activeById[id] + if type(row) == "table" then + row.migrationNote = "Auto-archived by 2.0 pet migration (orphaned active row)." + if MTH_PETS_MoveToHistory(pets, id, "migration-v3", nil, reason or "migration-recovered") then + archived = archived + 1 + end + end + end + if archived > 0 and MTH and MTH.DebugPrint then + MTH:DebugPrint("Pet migration: archived " .. tostring(archived) .. " orphan pet row(s) to history.") + end + return archived +end + function MTH_PETS_RecordPetAbandon(source, explicitName) local pets = MTH_PETS_GetRootStore() if type(pets) ~= "table" then @@ -3952,6 +4048,43 @@ function MTH_CommandPetsReset() return true end +-- Dev-only: force the v3 pet-store migration to run again (dedup by canonical key, +-- level-free re-sign, index rebuild, orphan cleanup). Resets the pet schema flag +-- and re-runs EnsureSchema. Does NOT touch MTH.version, so nothing is broadcast. +-- Non-destructive: history is preserved. Invoke via /mth dev petmigrate or /run. +function MTH_CommandPetsMigrate() + local pets = MTH_PETS_GetRootStore() + if type(pets) ~= "table" then + MTH:Print("Pet migration failed: datastore unavailable.") + return false + end + local store = MTH_PETS_GetStoreTables(pets) + local function countTable(t) + local n = 0 + if type(t) == "table" then for _ in pairs(t) do n = n + 1 end end + return n + end + local beforeActive = countTable(store and store.activeById) + local beforeHistory = countTable(store and store.historyById) + + -- Force the one-shot path to re-run regardless of current stamp. + pets.schemaVersion = 2 + MTH_PETS_EnsureSchema(pets) + + store = MTH_PETS_GetStoreTables(pets) + local afterActive = countTable(store and store.activeById) + local afterHistory = countTable(store and store.historyById) + pets.updatedAt = time() + if type(MTH_CharSavedVariables) == "table" then + MTH_CharSavedVariables.MTH_Pets = pets + MTH_CharSavedVariables.petStore = pets.petStore + end + MTH:Print("Pet migration (v3) complete: active " .. tostring(beforeActive) .. "->" .. tostring(afterActive) + .. ", history " .. tostring(beforeHistory) .. "->" .. tostring(afterHistory) + .. ", schemaVersion=" .. tostring(pets.schemaVersion)) + return true +end + function MTH_CommandPetsDump() local pets = MTH_PETS_GetRootStore() MTH_PETS_RefreshCurrentPet() diff --git a/api/core.lua b/api/core.lua index 4308a8e..dc1d2da 100644 --- a/api/core.lua +++ b/api/core.lua @@ -422,6 +422,16 @@ function SlashCmdList.MTH(msg, editbox) MTH:Print("|cffaaffaaArmed.|r Open any vendor, trainer, or stable master to capture their ID.") end end + elseif lowerCmd == "dev" then + if lowerArg == "petmigrate" then + if type(MTH_CommandPetsMigrate) == "function" then + MTH_CommandPetsMigrate() + else + MTH:Print("Pet migration command unavailable.") + end + else + MTH:Print("Dev commands: /mth dev petmigrate") + end elseif lowerCmd == "food" then local sub = lowerArg local function MTH_FoodItemLabel(itemId) diff --git a/api/minimap-button.lua b/api/minimap-button.lua index 947bd89..26eed87 100644 --- a/api/minimap-button.lua +++ b/api/minimap-button.lua @@ -25,10 +25,19 @@ local function MTH_MB_GetStore() if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.minimapButton) ~= "table" then - MTH_CharSavedVariables.minimapButton = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("minimapbutton") + end + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.minimapbutton) ~= "table" then + MTH_CharSavedVariables.modules.minimapbutton = {} + end + store = MTH_CharSavedVariables.modules.minimapbutton end - local store = MTH_CharSavedVariables.minimapButton if store.angle == nil then store.angle = 220 end diff --git a/api/options-autoquest.lua b/api/options-autoquest.lua index 032ade4..13c7a5f 100644 --- a/api/options-autoquest.lua +++ b/api/options-autoquest.lua @@ -16,26 +16,30 @@ local function MTH_AQ_EnsureStore() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoquest) ~= "table" then - MTH_CharSavedVariables.autoquest = {} + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autoquest") end - local store = MTH_CharSavedVariables.autoquest - if type(MTH_CharSavedVariables.questautomation) == "table" then - local legacy = MTH_CharSavedVariables.questautomation - if store.scorpokDrazial == nil and legacy.scorpokDrazial ~= nil then - store.scorpokDrazial = legacy.scorpokDrazial and true or false + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} end - if store.scorpokTooltip == nil and legacy.scorpokTooltip ~= nil then - store.scorpokTooltip = legacy.scorpokTooltip and true or false + if type(MTH_CharSavedVariables.modules.autoquest) ~= "table" then + MTH_CharSavedVariables.modules.autoquest = {} end + store = MTH_CharSavedVariables.modules.autoquest end - if store.scorpokDrazial == nil then - store.scorpokDrazial = false + if type(store.settings) ~= "table" then + store.settings = {} end - if store.arrowsForSissies == nil then - store.arrowsForSissies = false + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false end - return store + if settings.arrowsForSissies == nil then + settings.arrowsForSissies = false + end + return settings end function MTH_SetupAutoQuestOptions() diff --git a/api/options-credits.lua b/api/options-credits.lua index 194b303..25603ac 100644 --- a/api/options-credits.lua +++ b/api/options-credits.lua @@ -129,7 +129,7 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_ZHUNTER", - "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zAmmo, zCompanions, zMount, zToys.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), + "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zBars, zAmmo, zCompanions, zMount, zToys, zCraft.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, @@ -170,7 +170,7 @@ function MTH_SetupCreditsOptions() -4) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_MAP", "- The Map/Marker system is the one of pfQuest from Master Shagu. Unfortunately he is no more reachable for some months and I could not have some talk with him about this. I mostly let it untouched, I just made slight modifications so it can integrate better within Metahunt, and doesn't conflict with pfQuest."), + MTH_CR_L("CREDITS_ABOUT_MAP", "- The Map/Marker system is the one of pfQuest from Master Shagu. I mostly let it untouched, I just made slight modifications so it can integrate better within Metahunt, and doesn't conflict with pfQuest."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, @@ -179,8 +179,8 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_DATA_TITLE", "About data sources"), "GameFontNormal", -14, 1.00, 0.82, 0.00) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\n" - .. "Sources used to build the data include:"), + MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (twow 1.18.1). The beast information is coming directly from a extract of live twow DB, made for MetaHunt by twow dev lead in March 2026.\n\n" + .. "Other sources used to build the data include:"), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, diff --git a/api/savedvariables.lua b/api/savedvariables.lua new file mode 100644 index 0000000..d6687fe --- /dev/null +++ b/api/savedvariables.lua @@ -0,0 +1,434 @@ +------------------------------------------------------ +-- 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 = , +-- modules = { +-- = { schemaVersion = N, settings = { ...camelCase... }, }, +-- feedomatic = { settings{...}, foodQuality, cooking, questFood, +-- addedFoods, removedFoods, localeInfo }, +-- }, +-- messages = {...}, moduleStates = {...}, profiles = {...}, +-- charSnapshots = {...}, versionCheck = {...}, +-- } +-- MTH_CharSavedVariables (per-character) = { +-- schemaVersion = , +-- modules = { +-- = { schemaVersion = N, settings{...}, }, +-- 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 diff --git a/init/api.xml b/init/api.xml index a73951e..c964985 100644 --- a/init/api.xml +++ b/init/api.xml @@ -1,6 +1,7 @@ + diff --git a/modules/autoquest/module.lua b/modules/autoquest/module.lua index cb3fed7..69b836a 100644 --- a/modules/autoquest/module.lua +++ b/modules/autoquest/module.lua @@ -56,70 +56,33 @@ local function QA_GetOptionsStore() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoquest) ~= "table" then - MTH_CharSavedVariables.autoquest = {} - end - local store = MTH_CharSavedVariables.autoquest - - if store._migrated ~= true then - if type(MTH_CharSavedVariables.questautomation) == "table" then - local legacyStore = MTH_CharSavedVariables.questautomation - if legacyStore.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyStore.scorpokDrazial and true or false - end - if legacyStore.scorpokTooltip ~= nil and store.scorpokTooltip == nil then - store.scorpokTooltip = legacyStore.scorpokTooltip and true or false - end - end - local legacyChar = nil - local legacyAccount = nil - if MTH and MTH.GetModuleCharSavedVariables then - legacyChar = MTH:GetModuleCharSavedVariables("autoquest") - if type(legacyChar) ~= "table" then - legacyChar = MTH:GetModuleCharSavedVariables("questautomation") - end - end - if MTH and MTH.GetModuleSavedVariables then - legacyAccount = MTH:GetModuleSavedVariables("autoquest") - if type(legacyAccount) ~= "table" then - legacyAccount = MTH:GetModuleSavedVariables("questautomation") - end - end - if type(legacyChar) == "table" and legacyChar.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyChar.scorpokDrazial and true or false - elseif type(legacyAccount) == "table" and legacyAccount.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyAccount.scorpokDrazial and true or false - elseif store.scorpokDrazial == nil and MTH and MTH.GetModuleCharSavedVariables then - local autobuyStore = MTH:GetModuleCharSavedVariables("autobuy") - if type(autobuyStore) == "table" - and type(autobuyStore.questAutomation) == "table" - and autobuyStore.questAutomation.scorpokDrazial ~= nil then - store.scorpokDrazial = autobuyStore.questAutomation.scorpokDrazial and true or false - end - end - store._migrated = true - end - - if store.scorpokDrazial == nil then - store.scorpokDrazial = false - end - if store.arrowsForSissies == nil then - store.arrowsForSissies = false - end - if store.scorpokTooltip == nil then - store.scorpokTooltip = false - end - + local store = nil if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokDrazial = store.scorpokDrazial and true or false - moduleStore.arrowsForSissies = store.arrowsForSissies and true or false - moduleStore.scorpokTooltip = store.scorpokTooltip and true or false - end + store = MTH:GetModuleCharSavedVariables("autoquest") end - - return store + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.autoquest) ~= "table" then + MTH_CharSavedVariables.modules.autoquest = {} + end + store = MTH_CharSavedVariables.modules.autoquest + end + if type(store.settings) ~= "table" then + store.settings = {} + end + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false + end + if settings.arrowsForSissies == nil then + settings.arrowsForSissies = false + end + if settings.scorpokTooltip == nil then + settings.scorpokTooltip = false + end + return settings end function MTH_AutoQuest:GetStore() @@ -129,36 +92,18 @@ end function MTH_AutoQuest:SetScorpokDrazialEnabled(enabled) local store = QA_GetOptionsStore() store.scorpokDrazial = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokDrazial = store.scorpokDrazial and true or false - end - end QA_Debug("option write: scorpokDrazial=" .. tostring(store.scorpokDrazial and true or false)) end function MTH_AutoQuest:SetArrowsForSissiesEnabled(enabled) local store = QA_GetOptionsStore() store.arrowsForSissies = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.arrowsForSissies = store.arrowsForSissies and true or false - end - end QA_Debug("option write: arrowsForSissies=" .. tostring(store.arrowsForSissies and true or false)) end function MTH_AutoQuest:SetScorpokTooltipEnabled(enabled) local store = QA_GetOptionsStore() store.scorpokTooltip = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokTooltip = store.scorpokTooltip and true or false - end - end QA_Debug("option write: scorpokTooltip=" .. tostring(store.scorpokTooltip and true or false)) end diff --git a/modules/feedomatic/FeedOMatic.lua b/modules/feedomatic/FeedOMatic.lua index 4f5c1c9..322fc92 100644 --- a/modules/feedomatic/FeedOMatic.lua +++ b/modules/feedomatic/FeedOMatic.lua @@ -650,58 +650,6 @@ function MTH_GetMerchantFoodsByDiet() return result; end -local function FOM_ItemQuality(itemIdOrLink) - if (type(GetItemInfo) ~= "function" or itemIdOrLink == nil) then - return nil; - end - local _, _, quality = GetItemInfo(itemIdOrLink); - return quality; -end - -local function FOM_DebugLog(message) - if (not (FOM_Config and FOM_Config.Debug)) then - return; - end - local text = "[FOM] " .. tostring(message); - if (MTH and MTH.Print) then - MTH:Print(text, "debug"); - elseif (DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage) then - DEFAULT_CHAT_FRAME:AddMessage(text); - end - return; -end - -local function FOM_DebugLogMerchantFoods(sourceEvent) - local merchantFoods = MTH_GetMerchantFoodsByDiet(); - local eventLabel = tostring(sourceEvent or "MERCHANT"); - if (merchantFoods == nil) then - FOM_DebugLog(eventLabel .. ": no merchant data available."); - return; - end - - local knownCount = table.getn(merchantFoods.items or {}); - local unknownCount = table.getn(merchantFoods.unknown or {}); - local merchantName = UnitName("npc") or UnitName("target") or "unknown"; - - FOM_DebugLog(eventLabel .. ": " .. tostring(merchantName) .. " | known foods=" .. tostring(knownCount) .. " | unknown items=" .. tostring(unknownCount)); - - if (type(merchantFoods.byDiet) == "table") then - for dietName, foodRows in merchantFoods.byDiet do - local dietCount = table.getn(foodRows or {}); - FOM_DebugLog("diet " .. tostring(dietName) .. ": " .. tostring(dietCount) .. " item(s)"); - for _, row in foodRows do - FOM_DebugLog(" - [" .. tostring(row.id) .. "] " .. tostring(row.name) .. " (merchantIndex=" .. tostring(row.index) .. ")"); - end - end - end - - if (unknownCount > 0) then - for _, row in merchantFoods.unknown do - FOM_DebugLog("unknown food map: [" .. tostring(row.id) .. "] " .. tostring(row.name) .. " (merchantIndex=" .. tostring(row.index) .. ")"); - end - end -end - function MTH_FOM_FeedButton_OnClick() if (arg1 == "RightButton") then if (MTH_OpenOptions) then @@ -920,10 +868,6 @@ function FOM_OnEvent(event, arg1) end return; - elseif ( event == "MERCHANT_SHOW" or event == "MERCHANT_UPDATE" ) then - FOM_DebugLogMerchantFoods(event); - return; - elseif ( event == "PET_ATTACK_START" ) then -- Set Flag @@ -950,7 +894,6 @@ function FOM_OnEvent(event, arg1) end if (foodID) then FOM_LastFood = GFWUtils.ItemLink(foodID); - FOM_DebugLog("Manually fed "..FOM_LastFood); end end return; @@ -1269,31 +1212,24 @@ end function FOM_CanFeed() local petInfo = MTH_FOM_GetCorePetInfo(); if ( not (petInfo and petInfo.liveExists) ) then - FOM_DebugLog("Can't feed; pet doesn't exist."); return false; end if ( tonumber(petInfo.health) and tonumber(petInfo.health) <= 0 ) then - FOM_DebugLog("Can't feed; pet is dead."); return false; end if ( UnitHealth("player") <= 0 ) then - FOM_DebugLog("Can't feed; I'm dead."); return false; end if ( CastingBarFrameStatusBar:IsVisible() ) then - FOM_DebugLog("Can't feed; casting a spell / tradeksill."); return false; end if ( UnitOnTaxi("player") ) then - FOM_DebugLog("Can't feed; flying."); return false; end if ( FOM_State.InCombat ) or ( PlayerFrame.inCombat ) then - FOM_DebugLog("Can't feed; in combat."); return false; end if ( LootFrame:IsVisible() ) then - FOM_DebugLog("Shouldn't feed; loot window is open."); return false; end @@ -1315,14 +1251,12 @@ function FOM_CanFeed() local name = GetSpellRecField(sid, "name") if name then if dontFeedNames[name] then - FOM_DebugLog("Can't feed; buff detected: " .. name) return false end if UnitLevel("player") >= 40 then local lname = string.lower(name) for _, mountSub in FOM_MOUNT_NAME_SUBSTRINGS do if string.find(lname, mountSub) then - FOM_DebugLog("Can't feed; mounted (" .. name .. ").") return false end end @@ -1351,7 +1285,6 @@ function FOM_CanFeed() if ( buff ~= nil) then for _, buffTexture in dontFeedBuffTextures do if ( buff == buffTexture ) then - FOM_DebugLog("Can't feed; currently, eating, drinking, or feigning death."); return false; end end @@ -1364,7 +1297,6 @@ function FOM_CanFeed() msg = string.lower(msg); for _, mountName in FOM_MOUNT_NAME_SUBSTRINGS do if (string.find(msg, mountName)) then - FOM_DebugLog("Can't feed; mounted."); return false; end end @@ -2387,17 +2319,6 @@ function FOM_Feed(aFood, options) foodLevel = selectedFoodLevel, }) - FOM_DebugLog("Picked "..tostring(FOM_LastFood) - .." id="..tostring(selectedId) - .." foodLevel="..tostring(selectedFoodLevel) - .." itemQuality="..tostring(FOM_ItemQuality(selectedId)) - .." (bag "..tostring(foodBag)..", slot "..tostring(foodItem)..")" - .." reason="..tostring(FOM_LastChoiceReason)); - if (FOM_Config.Debug) then - -- don't actually feed anything, just show what we would choose - return false; - end - -- Actually feed the item to the pet PickupContainerItem(foodBag, foodItem); if ( CursorHasItem() ) then @@ -2610,19 +2531,6 @@ function FOM_FlatFoodList() if (table.getn(foodList) == 0 and table.getn(overflowFoodList) > 0) then foodList = overflowFoodList; end - if (FOM_Config and FOM_Config.Debug) then - FOM_DebugLog("Candidate foods for pet level "..tostring(petLevel).." ("..tostring(table.getn(foodList)).." in diet):"); - for i = 1, table.getn(foodList) do - local c = foodList[i]; - FOM_DebugLog(" ["..tostring(i).."] "..tostring(c.link) - .." id="..tostring(c.itemId) - .." qty="..tostring(c.count) - .." foodLevel="..tostring(c.quality) - .." itemQuality="..tostring(FOM_ItemQuality(c.itemId)) - .." known="..tostring(c.knownLevel) - .." useful="..tostring(c.useful)); - end - end return foodList; end @@ -2786,12 +2694,10 @@ end function FOM_IsUsefulFood(itemID, quantity) local foodName = GetItemInfo(itemID); if (foodName == nil) then - FOM_DebugLog("Can't get info for item ID "..itemID..", assuming it's OK to eat."); return false; end if (FOM_Cooking and FOM_Cooking[FOM_RealmPlayer] and FOM_Cooking[FOM_RealmPlayer][itemID]) then if (FOM_Cooking[FOM_RealmPlayer][itemID] >= FOM_Config.SaveForCookingLevel) then - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is good for cooking."); return true; end end @@ -2803,19 +2709,15 @@ function FOM_IsUsefulFood(itemID, quantity) FOM_Quantity[foodName] = FOM_Quantity[foodName] + quantity; end if (FOM_Quantity[foodName] > FOM_QuestFood[FOM_RealmPlayer][foodName]) then - FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; is needed for quest, but we have more than enough."); return false; else - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is needed for quest."); return true; end end end if (FOM_Config.AvoidBonusFood and FOM_IsInDiet(itemID, FOM_DIET_BONUS)) then - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; has bonus effect when eaten by player."); return true; end - --FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; doesn't have other uses."); return false; end diff --git a/modules/feedomatic/module.lua b/modules/feedomatic/module.lua index 8dad1d8..47637e5 100644 --- a/modules/feedomatic/module.lua +++ b/modules/feedomatic/module.lua @@ -8,8 +8,6 @@ local MTH_FeedOMatic = { enabled = false, events = { "VARIABLES_LOADED", - "MERCHANT_SHOW", - "MERCHANT_UPDATE", "MTH_PET_LIVE_STATE_CHANGED", "PET_ATTACK_START", "PET_ATTACK_STOP", @@ -31,7 +29,6 @@ local MTH_FeedOMatic = { } local MTH_PETL_HOOK_BOUNDARY_KEY = "pet-lifecycle-hooks" -local MTH_FOM_MerchantProbe = nil function MTH_FeedOMatic_OnUpdate(elapsed) if not elapsed then @@ -128,104 +125,34 @@ local function MTH_FeedOMatic_SyncSavedVariables() end local moduleStore = MTH:GetModuleSavedVariables("feedomatic") - if not moduleStore.legacy then - moduleStore.legacy = {} - end - local function bindLegacyTable(globalName) - local legacyValue = moduleStore.legacy[globalName] - local globalValue = _G and _G[globalName] or nil - - if type(legacyValue) ~= "table" then + -- Bind each legacy FeedOMatic global to its clean nested location under + -- modules.feedomatic. The globals are runtime aliases only (NOT persisted via + -- the TOC any more); the real data lives in these nested tables inside + -- MTH_SavedVariables. Adopt any pre-existing runtime global on first bind. + local function bindGlobal(globalName, key) + local target = moduleStore[key] + if type(target) ~= "table" then + local globalValue = _G and _G[globalName] or nil if type(globalValue) == "table" then - legacyValue = globalValue + target = globalValue else - legacyValue = {} + target = {} end - moduleStore.legacy[globalName] = legacyValue + moduleStore[key] = target end - if _G then - _G[globalName] = legacyValue + _G[globalName] = target end end - bindLegacyTable("FOM_Config") - bindLegacyTable("FOM_FoodQuality") - bindLegacyTable("FOM_AddedFoods") - bindLegacyTable("FOM_RemovedFoods") - bindLegacyTable("FOM_Cooking") - bindLegacyTable("FOM_QuestFood") - bindLegacyTable("FOM_LocaleInfo") -end - -local function MTH_FeedOMatic_Log(message, severity) - if type(MTH_Log) == "function" then - MTH_Log("[FeedOMatic] " .. tostring(message or ""), severity) - end -end - -local function MTH_FeedOMatic_DebugMerchantScan(sourceEvent) - if type(MTH_GetMerchantFoodsByDiet) ~= "function" then - MTH_FeedOMatic_Log(tostring(sourceEvent) .. ": MTH_GetMerchantFoodsByDiet is not available yet") - return - end - - local merchantFoods = MTH_GetMerchantFoodsByDiet() - local knownCount = 0 - local unknownCount = 0 - if type(merchantFoods) == "table" then - if type(merchantFoods.items) == "table" then - knownCount = table.getn(merchantFoods.items) - end - if type(merchantFoods.unknown) == "table" then - unknownCount = table.getn(merchantFoods.unknown) - end - end - - local merchantName = UnitName("npc") or UnitName("target") or "unknown" - MTH_FeedOMatic_Log(tostring(sourceEvent) .. ": wrapper scan for " .. tostring(merchantName) .. " | known=" .. tostring(knownCount) .. " | unknown=" .. tostring(unknownCount)) - - if type(merchantFoods.byDiet) == "table" then - for dietName, rows in merchantFoods.byDiet do - local dietCount = 0 - if type(rows) == "table" then - dietCount = table.getn(rows) - end - if type(rows) == "table" then - for _, row in rows do - end - end - end - end - - if unknownCount > 0 and type(merchantFoods.unknown) == "table" then - for _, row in merchantFoods.unknown do - end - end -end - -local function MTH_FeedOMatic_SetMerchantProbeEnabled(enabled) - if not enabled then - if MTH_FOM_MerchantProbe then - MTH_FOM_MerchantProbe:UnregisterEvent("MERCHANT_SHOW") - MTH_FOM_MerchantProbe:UnregisterEvent("MERCHANT_UPDATE") - end - return - end - - if not MTH_FOM_MerchantProbe then - MTH_FOM_MerchantProbe = CreateFrame("Frame", "MTH_FOM_MerchantProbe") - MTH_FOM_MerchantProbe:SetScript("OnEvent", function() - local evt = event - if evt == "MERCHANT_SHOW" or evt == "MERCHANT_UPDATE" then - MTH_FeedOMatic_DebugMerchantScan(evt) - end - end) - end - - MTH_FOM_MerchantProbe:RegisterEvent("MERCHANT_SHOW") - MTH_FOM_MerchantProbe:RegisterEvent("MERCHANT_UPDATE") + bindGlobal("FOM_Config", "settings") + bindGlobal("FOM_FoodQuality", "foodQuality") + bindGlobal("FOM_AddedFoods", "addedFoods") + bindGlobal("FOM_RemovedFoods", "removedFoods") + bindGlobal("FOM_Cooking", "cooking") + bindGlobal("FOM_QuestFood", "questFood") + bindGlobal("FOM_LocaleInfo", "localeInfo") end function MTH_FeedOMatic:init() @@ -265,7 +192,6 @@ function MTH_FeedOMatic:setEnabled(enabled) MTH_FeedOMatic_CaptureHookBoundary() MTH_FeedOMatic_SyncSavedVariables() MTH_FeedOMatic_EnsureVariablesLoaded(self) - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Show() end @@ -275,7 +201,6 @@ function MTH_FeedOMatic:setEnabled(enabled) if type(FOM_State) == "table" then FOM_State.ShouldFeed = false end - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Hide() end @@ -304,7 +229,6 @@ function MTH_FeedOMatic:cleanup() if type(FOM_State) == "table" then FOM_State.ShouldFeed = false end - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Hide(); end diff --git a/modules/smartammo/engine.lua b/modules/smartammo/engine.lua index 8cc19c2..6d6f14f 100644 --- a/modules/smartammo/engine.lua +++ b/modules/smartammo/engine.lua @@ -1,7 +1,3 @@ -local function MTHSmartAmmo_GetSaved() - return MTH_SA_GetSavedTable("MTHSmartAmmo") -end - local function MTHSmartAmmo_GetModuleStore() if MTH and MTH.GetModuleCharSavedVariables then return MTH:GetModuleCharSavedVariables("smartammo") @@ -9,40 +5,43 @@ local function MTHSmartAmmo_GetModuleStore() return nil end -local function MTHSmartAmmo_IsSmartEnabled() +-- Single source of truth: moduleStore.settings.{smartEnabled,reloadEnabled,weaponSwapEnabled} +local function MTHSmartAmmo_GetSettings() local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.smartEnabled ~= nil then - return moduleStore.smartEnabled and true or false + if type(moduleStore) ~= "table" then + return nil end - local saved = MTHSmartAmmo_GetSaved() - if saved["enabled"] == nil then - return true + if type(moduleStore.settings) ~= "table" then + moduleStore.settings = {} end - return saved["enabled"] and true or false + return moduleStore.settings +end + +local function MTHSmartAmmo_ReadFlag(key) + local settings = MTHSmartAmmo_GetSettings() + if settings and settings[key] ~= nil then + return settings[key] and true or false + end + return true -- default enabled +end + +local function MTHSmartAmmo_WriteFlag(key, enabled) + local settings = MTHSmartAmmo_GetSettings() + if settings then + settings[key] = enabled and true or false + end +end + +local function MTHSmartAmmo_IsSmartEnabled() + return MTHSmartAmmo_ReadFlag("smartEnabled") end local function MTHSmartAmmo_IsReloadEnabled() - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.reloadEnabled ~= nil then - return moduleStore.reloadEnabled and true or false - end - local saved = MTHSmartAmmo_GetSaved() - if saved["reload"] == nil then - return true - end - return saved["reload"] and true or false + return MTHSmartAmmo_ReadFlag("reloadEnabled") end local function MTHSmartAmmo_IsWeaponSwapEnabled() - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.weaponSwapEnabled ~= nil then - return moduleStore.weaponSwapEnabled and true or false - end - local saved = MTHSmartAmmo_GetSaved() - if saved["weaponSwap"] == nil then - return true - end - return saved["weaponSwap"] and true or false + return MTHSmartAmmo_ReadFlag("weaponSwapEnabled") end local MTHSmartAmmo_InitializeHooks @@ -58,9 +57,7 @@ local MTH_SA_SPELL_EVENTS = { } function MTHSmartAmmo_SetSmartEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() if enabled then - saved["enabled"] = 1 local evFrame = getglobal("MTH_SA_EventFrame") if evFrame then for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do @@ -77,7 +74,6 @@ function MTHSmartAmmo_SetSmartEnabled(enabled, silent) MTH_SA_Print("Smart Ammo Enabled.") end else - saved["enabled"] = false local evFrame = getglobal("MTH_SA_EventFrame") if evFrame then for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do @@ -89,10 +85,7 @@ function MTHSmartAmmo_SetSmartEnabled(enabled, silent) end end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.smartEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("smartEnabled", enabled and true or false) end function MTHSmartAmmo_GetSmartEnabled() @@ -100,16 +93,7 @@ function MTHSmartAmmo_GetSmartEnabled() end function MTHSmartAmmo_SetReloadEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() - if enabled then - saved["reload"] = 1 - else - saved["reload"] = false - end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.reloadEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("reloadEnabled", enabled and true or false) if not silent and DEFAULT_CHAT_FRAME then MTH_SA_Print("Smart Ammo reload fallback " .. (enabled and "Enabled." or "Disabled.")) end @@ -120,16 +104,7 @@ function MTHSmartAmmo_GetReloadEnabled() end function MTHSmartAmmo_SetWeaponSwapEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() - if enabled then - saved["weaponSwap"] = 1 - else - saved["weaponSwap"] = false - end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.weaponSwapEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("weaponSwapEnabled", enabled and true or false) if not silent and DEFAULT_CHAT_FRAME then MTH_SA_Print("Smart Ammo weapon-swap auto-equip " .. (enabled and "Enabled." or "Disabled.")) end @@ -807,15 +782,15 @@ SlashCmdList["MTHSmartAmmo"] = function(msg) return end - local saved = MTHSmartAmmo_GetSaved() + local smartEnabled = MTHSmartAmmo_GetSmartEnabled() if msg == "status" then local castHook, actionHook, byNameHook = MTHSmartAmmo_AreHooksInstalled() - MTH_SA_Print("Smart Ammo status: smart=" .. tostring(saved["enabled"] and true or false) .. ", module=" .. tostring(MTH_SA_IsModuleEnabled()) .. ", hooks=" .. tostring(castHook) .. "/" .. tostring(actionHook) .. "/" .. tostring(byNameHook)) + MTH_SA_Print("Smart Ammo status: smart=" .. tostring(smartEnabled) .. ", module=" .. tostring(MTH_SA_IsModuleEnabled()) .. ", hooks=" .. tostring(castHook) .. "/" .. tostring(actionHook) .. "/" .. tostring(byNameHook)) return end - - if saved["enabled"] then + + if smartEnabled then MTHSmartAmmo_SetSmartEnabled(nil) else MTHSmartAmmo_SetSmartEnabled(1) diff --git a/modules/smartammo/module.lua b/modules/smartammo/module.lua index ccb4c11..54723d4 100644 --- a/modules/smartammo/module.lua +++ b/modules/smartammo/module.lua @@ -15,39 +15,6 @@ local MTH_SmartAmmo = { local MTH_SA_BOUNDARY_KEY = "smartammo.core" -function MTH_SA_GetSavedTable(tableName) - if not MTH or not MTH.GetModuleCharSavedVariables then - if type(_G[tableName]) ~= "table" then - _G[tableName] = {} - end - return _G[tableName] - end - - local moduleStore = MTH:GetModuleCharSavedVariables("smartammo") - if type(moduleStore) ~= "table" then - moduleStore = {} - end - - if not moduleStore.legacy then - moduleStore.legacy = {} - end - if type(moduleStore.legacy[tableName]) ~= "table" then - moduleStore.legacy[tableName] = {} - end - - if type(_G[tableName]) == "table" then - local legacyTable = moduleStore.legacy[tableName] - if not next(legacyTable) then - for key, value in pairs(_G[tableName]) do - legacyTable[key] = value - end - end - end - _G[tableName] = moduleStore.legacy[tableName] - - return moduleStore.legacy[tableName] -end - function MTH_SA_IsModuleEnabled() if MTH and MTH.IsModuleEnabled then return MTH:IsModuleEnabled("smartammo", true) and true or false @@ -70,43 +37,22 @@ local function MTH_SA_ApplySavedState() return end - local saved = MTH_SA_GetSavedTable("MTHSmartAmmo") local moduleStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("smartammo") or nil - if type(moduleStore) == "table" then - if moduleStore.smartEnabled ~= nil then - saved["enabled"] = moduleStore.smartEnabled and 1 or false - end - if moduleStore.reloadEnabled ~= nil then - saved["reload"] = moduleStore.reloadEnabled and 1 or false - end - if moduleStore.weaponSwapEnabled ~= nil then - saved["weaponSwap"] = moduleStore.weaponSwapEnabled and 1 or false - end - end - if saved["enabled"] == nil then - saved["enabled"] = 1 - if type(moduleStore) == "table" then - moduleStore.smartEnabled = true - end - end - if saved["reload"] == nil and type(moduleStore) == "table" then - moduleStore.reloadEnabled = saved["reload"] ~= false - end - if saved["weaponSwap"] == nil then - saved["weaponSwap"] = 1 - if type(moduleStore) == "table" then - moduleStore.weaponSwapEnabled = true - end - elseif type(moduleStore) == "table" and moduleStore.weaponSwapEnabled == nil then - moduleStore.weaponSwapEnabled = saved["weaponSwap"] ~= false - end + local settings = (type(moduleStore) == "table" and type(moduleStore.settings) == "table") and moduleStore.settings or {} - MTHSmartAmmo_SetSmartEnabled(saved["enabled"] and 1 or nil, 1) + local smart = settings.smartEnabled + if smart == nil then smart = true end + local reload = settings.reloadEnabled + if reload == nil then reload = true end + local swap = settings.weaponSwapEnabled + if swap == nil then swap = true end + + MTHSmartAmmo_SetSmartEnabled(smart and 1 or nil, 1) if type(MTHSmartAmmo_SetReloadEnabled) == "function" then - MTHSmartAmmo_SetReloadEnabled(saved["reload"] ~= false and 1 or nil, 1) + MTHSmartAmmo_SetReloadEnabled(reload and 1 or nil, 1) end if type(MTHSmartAmmo_SetWeaponSwapEnabled) == "function" then - MTHSmartAmmo_SetWeaponSwapEnabled(saved["weaponSwap"] ~= false and 1 or nil, 1) + MTHSmartAmmo_SetWeaponSwapEnabled(swap and 1 or nil, 1) end if type(MTHSmartAmmo_EnsureHooks) == "function" then MTHSmartAmmo_EnsureHooks("module-apply") @@ -154,7 +100,6 @@ local function MTH_SA_RestoreHooks() end function MTH_SmartAmmo:init() - MTH_SA_GetSavedTable("MTHSmartAmmo") if self.enabled then MTH_SA_ApplySavedState() end diff --git a/modules/tooltips/module.lua b/modules/tooltips/module.lua index 479b566..5f8f494 100644 --- a/modules/tooltips/module.lua +++ b/modules/tooltips/module.lua @@ -115,23 +115,24 @@ local function MTH_TT_IsOwnPetTooltipsEnabled() end local function MTH_TT_GetAutoQuestStore() - if type(MTH_CharSavedVariables) ~= "table" then - return nil - end - local store = MTH_CharSavedVariables.autoquest - if type(store) ~= "table" then - store = MTH_CharSavedVariables.questautomation + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autoquest") end if type(store) ~= "table" then return nil end - if store.scorpokDrazial == nil then - store.scorpokDrazial = false + if type(store.settings) ~= "table" then + store.settings = {} end - if store.scorpokTooltip == nil then - store.scorpokTooltip = false + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false end - return store + if settings.scorpokTooltip == nil then + settings.scorpokTooltip = false + end + return settings end local function MTH_TT_IsScorpokAutomationEnabled() diff --git a/modules/zhunter/ZHunterMod.lua b/modules/zhunter/ZHunterMod.lua index cfeb15a..9f80e61 100644 --- a/modules/zhunter/ZHunterMod.lua +++ b/modules/zhunter/ZHunterMod.lua @@ -43,10 +43,35 @@ function MTH_ZH_IsModuleEnabled() end function MTH_ZH_GetSavedRoot() - if type(ZHunterMod_Saved) ~= "table" then - ZHunterMod_Saved = {} + -- Canonical store is MTH_CharSavedVariables.modules.zhunter. The old global + -- ZHunterMod_Saved is kept only as a RUNTIME alias so the many zButton files + -- and options-zbuttons.lua can keep referencing it unchanged. + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("zhunter") end - return ZHunterMod_Saved + + if type(store) ~= "table" then + -- Framework not ready yet: fall back to a temporary global holder; + -- MTH_ZH_SyncSavedVariables re-binds to the nested store later. + if type(ZHunterMod_Saved) ~= "table" then + ZHunterMod_Saved = {} + end + return ZHunterMod_Saved + end + + -- Adopt a pre-existing global (legacy TOC copy) once, only if the nested + -- store is still empty, so no layout is lost during the transition. + if type(ZHunterMod_Saved) == "table" and ZHunterMod_Saved ~= store then + if next(store) == nil then + for k, v in pairs(ZHunterMod_Saved) do + store[k] = v + end + end + end + + ZHunterMod_Saved = store -- pure runtime alias into the canonical store + return store end function MTH_ZH_GetSavedTable(key) diff --git a/modules/zhunter/module.lua b/modules/zhunter/module.lua index 2c2b438..4308c33 100644 --- a/modules/zhunter/module.lua +++ b/modules/zhunter/module.lua @@ -165,18 +165,11 @@ MTH_ZH_ApplyEnabledRuntimeState = function(source) end MTH_ZH_SyncSavedVariables = function() - if not MTH or not MTH.GetModuleCharSavedVariables then - return - end - - local moduleStore = MTH:GetModuleCharSavedVariables("zhunter") - - if type(ZHunterMod_Saved) == "table" then - if MTH_CharSavedVariables and MTH_CharSavedVariables.modules then - MTH_CharSavedVariables.modules.zhunter = ZHunterMod_Saved - end - elseif type(moduleStore) == "table" then - ZHunterMod_Saved = moduleStore + -- Bind the ZHunterMod_Saved global as a runtime alias into the canonical + -- nested store (MTH_CharSavedVariables.modules.zhunter). GetSavedRoot does the + -- adoption + aliasing; there is no second on-disk copy anymore. + if type(MTH_ZH_GetSavedRoot) == "function" then + MTH_ZH_GetSavedRoot() end end