mirror of
https://codeberg.org/Duvelcorp/MetaHunt.git
synced 2026-09-27 01:56:04 +00:00
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).
This commit is contained in:
+154
-21
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user