mirror of
https://codeberg.org/Duvelcorp/MetaHunt.git
synced 2026-09-22 07:36:58 +00:00
4234 lines
131 KiB
Lua
4234 lines
131 KiB
Lua
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_CORE_HOOK_BOUNDARY_KEY = "core-pet-rename-hook"
|
|
MTH_PETS_TRACE_CONSISTENCY = false
|
|
|
|
local MTH_PetLifecycleEventFrame = nil
|
|
local MTH_PetLifecycleBootstrapFrame = nil
|
|
local MTH_ST_LastAutoScanAt = 0
|
|
local MTH_ST_LastUnitPetHadPet = false
|
|
local MTH_ST_DeferredStableScanFrame = nil
|
|
local MTH_ST_Profile = {
|
|
enabled = false,
|
|
windowStart = 0,
|
|
windowSeconds = 5,
|
|
eventCount = 0,
|
|
totalMs = 0,
|
|
maxMs = 0,
|
|
byEvent = {},
|
|
runawayChecks = 0,
|
|
runawayMs = 0,
|
|
runawayMaxMs = 0,
|
|
}
|
|
local MTH_ST_Trace = {
|
|
enabled = false,
|
|
verbose = false,
|
|
windowStart = 0,
|
|
windowSeconds = 5,
|
|
eventCount = 0,
|
|
byEvent = {},
|
|
byRoute = {},
|
|
byNote = {},
|
|
}
|
|
local MTH_ST_Diag = {
|
|
lines = {},
|
|
maxLines = 200,
|
|
chatEcho = false,
|
|
}
|
|
local MTH_ST_DiagCopyFrame = nil
|
|
local MTH_ST_PrintStableDiag
|
|
local MTH_PETS_LastTameAttempt = nil
|
|
local MTH_PETS_LiveState = nil
|
|
local MTH_PETS_LiveStateSeq = 0
|
|
local MTH_PETS_LiveStateSubscribers = {}
|
|
local MTH_PETS_LiveStateEventName = "MTH_PET_LIVE_STATE_CHANGED"
|
|
local MTH_PETS_EventThrottle = {}
|
|
|
|
MTH_PETS_CoreOriginal_PetRename = MTH_PETS_CoreOriginal_PetRename or nil
|
|
|
|
local function MTH_ST_Log(line)
|
|
return
|
|
end
|
|
|
|
local function MTH_ST_TraceNow()
|
|
return (type(GetTime) == "function" and GetTime()) or (type(time) == "function" and time()) or nil
|
|
end
|
|
|
|
local function MTH_ST_TraceResetCounters()
|
|
MTH_ST_Trace.eventCount = 0
|
|
MTH_ST_Trace.byEvent = {}
|
|
MTH_ST_Trace.byRoute = {}
|
|
MTH_ST_Trace.byNote = {}
|
|
MTH_ST_Trace.windowStart = MTH_ST_TraceNow() or 0
|
|
end
|
|
|
|
local function MTH_ST_TraceCount(bucket, key)
|
|
if type(bucket) ~= "table" then
|
|
return
|
|
end
|
|
local k = tostring(key or "?")
|
|
bucket[k] = (tonumber(bucket[k]) or 0) + 1
|
|
end
|
|
|
|
local function MTH_ST_TraceBuildTopList(bucket, maxRows)
|
|
local rows = {}
|
|
for key, value in pairs(bucket or {}) do
|
|
table.insert(rows, { key = tostring(key), count = tonumber(value) or 0 })
|
|
end
|
|
table.sort(rows, function(a, b)
|
|
if a.count == b.count then
|
|
return a.key < b.key
|
|
end
|
|
return a.count > b.count
|
|
end)
|
|
local limit = math.min(tonumber(maxRows) or 4, table.getn(rows))
|
|
if limit <= 0 then
|
|
return "none"
|
|
end
|
|
local parts = {}
|
|
for i = 1, limit do
|
|
table.insert(parts, tostring(rows[i].key) .. ":" .. tostring(rows[i].count))
|
|
end
|
|
return table.concat(parts, ",")
|
|
end
|
|
|
|
local function MTH_ST_TraceRecord(evt, route, note)
|
|
if not (MTH_ST_Trace and MTH_ST_Trace.enabled) then
|
|
return
|
|
end
|
|
MTH_ST_Trace.eventCount = (tonumber(MTH_ST_Trace.eventCount) or 0) + 1
|
|
MTH_ST_TraceCount(MTH_ST_Trace.byEvent, evt)
|
|
MTH_ST_TraceCount(MTH_ST_Trace.byRoute, route)
|
|
if note and note ~= "" then
|
|
MTH_ST_TraceCount(MTH_ST_Trace.byNote, note)
|
|
end
|
|
|
|
local evtText = tostring(evt or "")
|
|
local routeText = tostring(route or "")
|
|
local noteText = tostring(note or "")
|
|
if string.find(evtText, "PET_STABLE_", 1, true)
|
|
or routeText == "scan"
|
|
or string.find(noteText, "stable", 1, true)
|
|
or string.find(noteText, "deferred", 1, true)
|
|
or string.find(noteText, "throttled", 1, true) then
|
|
MTH_ST_PrintStableDiag("trace evt=" .. evtText .. " route=" .. routeText .. " note=" .. noteText)
|
|
end
|
|
|
|
if MTH_ST_Trace.verbose and MTH and MTH.Print then
|
|
local hasPet = (type(UnitExists) == "function" and UnitExists("pet")) and true or false
|
|
local dead = (type(UnitIsDeadOrGhost) == "function" and UnitIsDeadOrGhost("player")) or false
|
|
MTH:Print("[STTRACE] evt=" .. tostring(evt)
|
|
.. " route=" .. tostring(route)
|
|
.. " note=" .. tostring(note or "-")
|
|
.. " pet=" .. tostring(hasPet)
|
|
.. " dead=" .. tostring(dead and true or false), "debug")
|
|
end
|
|
end
|
|
|
|
local function MTH_ST_TraceFlushIfDue(force)
|
|
if not (MTH_ST_Trace and MTH_ST_Trace.enabled) then
|
|
return
|
|
end
|
|
local now = MTH_ST_TraceNow()
|
|
if not now then
|
|
return
|
|
end
|
|
local started = tonumber(MTH_ST_Trace.windowStart) or 0
|
|
if started <= 0 then
|
|
MTH_ST_Trace.windowStart = now
|
|
return
|
|
end
|
|
local elapsed = now - started
|
|
local windowSeconds = tonumber(MTH_ST_Trace.windowSeconds) or 5
|
|
if not force and elapsed < windowSeconds then
|
|
return
|
|
end
|
|
if MTH and MTH.Print then
|
|
MTH:Print("[STTRACE] " .. tostring(string.format("%.1fs", elapsed))
|
|
.. " events=" .. tostring(tonumber(MTH_ST_Trace.eventCount) or 0)
|
|
.. " byEvent=" .. tostring(MTH_ST_TraceBuildTopList(MTH_ST_Trace.byEvent, 6))
|
|
.. " byRoute=" .. tostring(MTH_ST_TraceBuildTopList(MTH_ST_Trace.byRoute, 6))
|
|
.. " notes=" .. tostring(MTH_ST_TraceBuildTopList(MTH_ST_Trace.byNote, 6)),
|
|
"debug")
|
|
end
|
|
MTH_ST_TraceResetCounters()
|
|
end
|
|
|
|
local function MTH_ST_IsStableUiEvent(evt)
|
|
return evt == "PET_STABLE_SHOW"
|
|
or evt == "PET_STABLE_UPDATE"
|
|
or evt == "PET_STABLE_CLOSED"
|
|
end
|
|
|
|
MTH_ST_PrintStableDiag = function(line)
|
|
return
|
|
end
|
|
|
|
local function MTH_ST_BuildDiagDumpText()
|
|
local lines = MTH_ST_Diag.lines or {}
|
|
if table.getn(lines) <= 0 then
|
|
return "(no stable diagnostics captured yet)"
|
|
end
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local function MTH_ST_EnsureDiagCopyFrame()
|
|
if MTH_ST_DiagCopyFrame then
|
|
return MTH_ST_DiagCopyFrame
|
|
end
|
|
local frame = CreateFrame("Frame", "MTH_StableDiagCopy", UIParent)
|
|
if not frame then
|
|
return nil
|
|
end
|
|
frame:SetWidth(820)
|
|
frame:SetHeight(440)
|
|
frame:SetPoint("CENTER", UIParent, "CENTER")
|
|
frame:SetFrameStrata("DIALOG")
|
|
frame:SetBackdrop({
|
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
|
tile = true,
|
|
tileSize = 32,
|
|
edgeSize = 32,
|
|
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
|
})
|
|
frame:SetBackdropColor(0, 0, 0, 0.9)
|
|
frame:SetMovable(true)
|
|
frame:EnableMouse(true)
|
|
frame:RegisterForDrag("LeftButton")
|
|
frame:SetScript("OnDragStart", function() frame:StartMoving() end)
|
|
frame:SetScript("OnDragStop", function() frame:StopMovingOrSizing() end)
|
|
|
|
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
|
title:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, -15)
|
|
title:SetText("MetaHunt Stable Diagnostics")
|
|
|
|
local closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
|
closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -12)
|
|
closeButton:SetWidth(24)
|
|
closeButton:SetHeight(24)
|
|
closeButton:SetText("X")
|
|
closeButton:SetScript("OnClick", function() frame:Hide() end)
|
|
|
|
local copyButton = CreateFrame("Button", nil, frame, "GameMenuButtonTemplate")
|
|
copyButton:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 12)
|
|
copyButton:SetWidth(110)
|
|
copyButton:SetHeight(24)
|
|
copyButton:SetText("Select All")
|
|
|
|
local scrollFrame = CreateFrame("ScrollFrame", "MTH_StableDiagScroll", frame, "UIPanelScrollFrameTemplate")
|
|
scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -48)
|
|
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -36, 44)
|
|
|
|
local editBox = CreateFrame("EditBox", "MTH_StableDiagText", scrollFrame)
|
|
editBox:SetMultiLine(true)
|
|
editBox:SetMaxLetters(999999)
|
|
editBox:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
|
|
editBox:SetTextColor(0.85, 0.85, 0.85, 1)
|
|
editBox:SetWidth(760)
|
|
editBox:SetHeight(1)
|
|
editBox:SetAutoFocus(false)
|
|
editBox:EnableMouse(true)
|
|
editBox:EnableMouseWheel(true)
|
|
editBox:SetScript("OnEscapePressed", function() editBox:ClearFocus() end)
|
|
editBox:SetScript("OnMouseWheel", function()
|
|
local curr = scrollFrame:GetVerticalScroll()
|
|
if arg1 and arg1 > 0 then
|
|
scrollFrame:SetVerticalScroll(curr - 24)
|
|
else
|
|
scrollFrame:SetVerticalScroll(curr + 24)
|
|
end
|
|
end)
|
|
scrollFrame:SetScrollChild(editBox)
|
|
|
|
copyButton:SetScript("OnClick", function()
|
|
editBox:HighlightText(0, -1)
|
|
editBox:SetFocus()
|
|
end)
|
|
|
|
frame._scroll = scrollFrame
|
|
frame._edit = editBox
|
|
MTH_ST_DiagCopyFrame = frame
|
|
if UISpecialFrames then
|
|
local exists = false
|
|
for i = 1, table.getn(UISpecialFrames) do
|
|
if UISpecialFrames[i] == "MTH_StableDiagCopy" then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
table.insert(UISpecialFrames, "MTH_StableDiagCopy")
|
|
end
|
|
end
|
|
return frame
|
|
end
|
|
|
|
function MTH_CommandStableDiag(mode)
|
|
MTH_ST_Diag.lines = {}
|
|
MTH_ST_Diag.chatEcho = false
|
|
if MTH and MTH.Print then
|
|
MTH:Print("Stable diagnostics are disabled.")
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MTH_CommandStableTrace(mode)
|
|
MTH_ST_Trace.enabled = false
|
|
MTH_ST_Trace.verbose = false
|
|
if MTH and MTH.Print then
|
|
MTH:Print("Stable trace is disabled.")
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_ST_ProfileNow()
|
|
return (type(GetTime) == "function" and GetTime()) or (type(time) == "function" and time()) or nil
|
|
end
|
|
|
|
local function MTH_ST_ProfileResetCounters()
|
|
MTH_ST_Profile.eventCount = 0
|
|
MTH_ST_Profile.totalMs = 0
|
|
MTH_ST_Profile.maxMs = 0
|
|
MTH_ST_Profile.byEvent = {}
|
|
MTH_ST_Profile.runawayChecks = 0
|
|
MTH_ST_Profile.runawayMs = 0
|
|
MTH_ST_Profile.runawayMaxMs = 0
|
|
MTH_ST_Profile.windowStart = MTH_ST_ProfileNow() or 0
|
|
end
|
|
|
|
local function MTH_ST_ProfileRecordEvent(evt, elapsedMs)
|
|
if not (MTH_ST_Profile and MTH_ST_Profile.enabled) then
|
|
return
|
|
end
|
|
local eventName = tostring(evt or "?")
|
|
local elapsed = tonumber(elapsedMs) or 0
|
|
MTH_ST_Profile.eventCount = (tonumber(MTH_ST_Profile.eventCount) or 0) + 1
|
|
MTH_ST_Profile.totalMs = (tonumber(MTH_ST_Profile.totalMs) or 0) + elapsed
|
|
if elapsed > (tonumber(MTH_ST_Profile.maxMs) or 0) then
|
|
MTH_ST_Profile.maxMs = elapsed
|
|
end
|
|
if type(MTH_ST_Profile.byEvent[eventName]) ~= "table" then
|
|
MTH_ST_Profile.byEvent[eventName] = { count = 0, totalMs = 0, maxMs = 0 }
|
|
end
|
|
local row = MTH_ST_Profile.byEvent[eventName]
|
|
row.count = (tonumber(row.count) or 0) + 1
|
|
row.totalMs = (tonumber(row.totalMs) or 0) + elapsed
|
|
if elapsed > (tonumber(row.maxMs) or 0) then
|
|
row.maxMs = elapsed
|
|
end
|
|
end
|
|
|
|
local function MTH_ST_ProfileRecordRunaway(elapsedMs)
|
|
if not (MTH_ST_Profile and MTH_ST_Profile.enabled) then
|
|
return
|
|
end
|
|
local elapsed = tonumber(elapsedMs) or 0
|
|
MTH_ST_Profile.runawayChecks = (tonumber(MTH_ST_Profile.runawayChecks) or 0) + 1
|
|
MTH_ST_Profile.runawayMs = (tonumber(MTH_ST_Profile.runawayMs) or 0) + elapsed
|
|
if elapsed > (tonumber(MTH_ST_Profile.runawayMaxMs) or 0) then
|
|
MTH_ST_Profile.runawayMaxMs = elapsed
|
|
end
|
|
end
|
|
|
|
local function MTH_ST_ProfileBuildTopList(metricKey, maxRows)
|
|
local rows = {}
|
|
for eventName, row in pairs(MTH_ST_Profile.byEvent or {}) do
|
|
if type(row) == "table" then
|
|
table.insert(rows, {
|
|
event = tostring(eventName),
|
|
count = tonumber(row.count) or 0,
|
|
totalMs = tonumber(row.totalMs) or 0,
|
|
maxMs = tonumber(row.maxMs) or 0,
|
|
})
|
|
end
|
|
end
|
|
table.sort(rows, function(a, b)
|
|
local av = tonumber(a[metricKey]) or 0
|
|
local bv = tonumber(b[metricKey]) or 0
|
|
if av == bv then
|
|
return tostring(a.event or "") < tostring(b.event or "")
|
|
end
|
|
return av > bv
|
|
end)
|
|
|
|
local limit = math.min(tonumber(maxRows) or 3, table.getn(rows))
|
|
if limit <= 0 then
|
|
return "none"
|
|
end
|
|
|
|
local parts = {}
|
|
for i = 1, limit do
|
|
local row = rows[i]
|
|
if metricKey == "count" then
|
|
table.insert(parts, tostring(row.event) .. ":" .. tostring(row.count))
|
|
else
|
|
table.insert(parts, tostring(row.event) .. ":" .. string.format("%.2f", tonumber(row.totalMs) or 0) .. "ms")
|
|
end
|
|
end
|
|
return table.concat(parts, ",")
|
|
end
|
|
|
|
local function MTH_ST_ProfileFlushIfDue(force)
|
|
if not (MTH_ST_Profile and MTH_ST_Profile.enabled) then
|
|
return
|
|
end
|
|
local now = MTH_ST_ProfileNow()
|
|
if not now then
|
|
return
|
|
end
|
|
local windowStart = tonumber(MTH_ST_Profile.windowStart) or 0
|
|
if windowStart <= 0 then
|
|
MTH_ST_Profile.windowStart = now
|
|
return
|
|
end
|
|
local elapsed = now - windowStart
|
|
local windowSeconds = tonumber(MTH_ST_Profile.windowSeconds) or 5
|
|
if not force and elapsed < windowSeconds then
|
|
return
|
|
end
|
|
if MTH and MTH.Print then
|
|
local eventCount = tonumber(MTH_ST_Profile.eventCount) or 0
|
|
local totalMs = tonumber(MTH_ST_Profile.totalMs) or 0
|
|
local avgMs = (eventCount > 0) and (totalMs / eventCount) or 0
|
|
local maxMs = tonumber(MTH_ST_Profile.maxMs) or 0
|
|
local runawayChecks = tonumber(MTH_ST_Profile.runawayChecks) or 0
|
|
local runawayMs = tonumber(MTH_ST_Profile.runawayMs) or 0
|
|
local runawayAvg = (runawayChecks > 0) and (runawayMs / runawayChecks) or 0
|
|
local runawayMax = tonumber(MTH_ST_Profile.runawayMaxMs) or 0
|
|
local topCount = MTH_ST_ProfileBuildTopList("count", 4)
|
|
local topMs = MTH_ST_ProfileBuildTopList("totalMs", 4)
|
|
MTH:Print("[STPROF] " .. tostring(string.format("%.1fs", elapsed))
|
|
.. " events=" .. tostring(eventCount)
|
|
.. " total=" .. tostring(string.format("%.2f", totalMs)) .. "ms"
|
|
.. " avg=" .. tostring(string.format("%.3f", avgMs)) .. "ms"
|
|
.. " max=" .. tostring(string.format("%.3f", maxMs)) .. "ms"
|
|
.. " topCount=" .. tostring(topCount)
|
|
.. " topMs=" .. tostring(topMs)
|
|
.. " runaway=" .. tostring(runawayChecks)
|
|
.. "/" .. tostring(string.format("%.2f", runawayMs)) .. "ms"
|
|
.. " avg=" .. tostring(string.format("%.3f", runawayAvg)) .. "ms"
|
|
.. " max=" .. tostring(string.format("%.3f", runawayMax)) .. "ms",
|
|
"debug")
|
|
end
|
|
MTH_ST_ProfileResetCounters()
|
|
end
|
|
|
|
function MTH_CommandStableProfile(mode)
|
|
MTH_ST_Profile.enabled = false
|
|
if MTH and MTH.Print then
|
|
MTH:Print("Stable profiler is disabled.")
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_LogConsistency(line)
|
|
return
|
|
end
|
|
|
|
local function MTH_PETS_LogTame(line)
|
|
return
|
|
end
|
|
|
|
local function MTH_PETS_FormatRowConsistency(row)
|
|
if type(row) ~= "table" then
|
|
return "<nil-row>"
|
|
end
|
|
return "id=" .. tostring(row.id)
|
|
.. " name='" .. tostring(row.name or "") .. "'"
|
|
.. " family='" .. tostring(row.family or "") .. "'"
|
|
.. " level='" .. tostring(row.level or "") .. "'"
|
|
.. " signature='" .. tostring(row.signature or "") .. "'"
|
|
.. " guid='" .. tostring(row.guid or "") .. "'"
|
|
end
|
|
|
|
local function MTH_PETS_FormatCurrentConsistency(cp)
|
|
if type(cp) ~= "table" then
|
|
return "<nil-current>"
|
|
end
|
|
return "id=" .. tostring(cp.id)
|
|
.. " exists=" .. tostring(cp.exists)
|
|
.. " name='" .. tostring(cp.name or "") .. "'"
|
|
.. " family='" .. tostring(cp.family or "") .. "'"
|
|
.. " level='" .. tostring(cp.level or "") .. "'"
|
|
.. " signature='" .. tostring(cp.signature or "") .. "'"
|
|
.. " guid='" .. tostring(cp.guid or "") .. "'"
|
|
end
|
|
|
|
local function MTH_PETS_RequestPetSpellScan(reason)
|
|
local stableFrame = (type(getglobal) == "function" and getglobal("PetStableFrame")) or (_G and _G["PetStableFrame"])
|
|
if stableFrame and type(stableFrame.IsShown) == "function" and stableFrame:IsShown() then
|
|
MTH_ST_PrintStableDiag("swap spellscan-skip reason=stable-open src=" .. tostring(reason or "stablemaster-current-change"))
|
|
return
|
|
end
|
|
MTH_ST_PrintStableDiag("swap spellscan-request reason=" .. tostring(reason or "stablemaster-current-change"))
|
|
if type(MTH_PSP_RequestScan) == "function" then
|
|
MTH_PSP_RequestScan(tostring(reason or "stablemaster-current-change"), 1)
|
|
end
|
|
end
|
|
|
|
local function MTH_ST_IsStableFrameOpen()
|
|
local stableFrame = (type(getglobal) == "function" and getglobal("PetStableFrame")) or (_G and _G["PetStableFrame"])
|
|
return stableFrame and type(stableFrame.IsShown) == "function" and stableFrame:IsShown() and true or false
|
|
end
|
|
|
|
local function MTH_PETS_ShouldHandleThrottledEvent(key, minIntervalSeconds)
|
|
local now = (type(GetTime) == "function" and GetTime()) or (type(time) == "function" and time()) or 0
|
|
local threshold = tonumber(minIntervalSeconds) or 0
|
|
local last = tonumber(MTH_PETS_EventThrottle[key] or 0) or 0
|
|
if threshold > 0 and (now - last) < threshold then
|
|
return false
|
|
end
|
|
MTH_PETS_EventThrottle[key] = now
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_CopyLiveState(state)
|
|
if type(state) ~= "table" then
|
|
return nil
|
|
end
|
|
local copy = {}
|
|
for key, value in pairs(state) do
|
|
copy[key] = value
|
|
end
|
|
return copy
|
|
end
|
|
|
|
local function MTH_PETS_BuildLiveState(source, skipRefresh)
|
|
local info = (type(MTH_GetCurrentPetInfo) == "function") and MTH_GetCurrentPetInfo(skipRefresh and true or false) or nil
|
|
if type(info) ~= "table" then
|
|
info = {}
|
|
end
|
|
|
|
return {
|
|
exists = info.exists and true or false,
|
|
liveExists = info.liveExists and true or false,
|
|
dead = info.dead and true or false,
|
|
suppressed = info.suppressed and true or false,
|
|
suppressedReason = tostring(info.suppressedReason or ""),
|
|
id = info.id,
|
|
name = info.name,
|
|
family = info.family,
|
|
level = tonumber(info.level) or nil,
|
|
happiness = tonumber(info.happiness) or nil,
|
|
loyalty = tonumber(info.loyalty) or nil,
|
|
loyaltyLevel = tonumber(info.loyaltyLevel) or nil,
|
|
health = tonumber(info.health) or nil,
|
|
healthMax = tonumber(info.healthMax) or nil,
|
|
xp = tonumber(info.xp) or nil,
|
|
xpMax = tonumber(info.xpMax) or nil,
|
|
xpPercent = tonumber(info.xpPercent) or nil,
|
|
lastSource = info.lastSource,
|
|
lastUpdated = tonumber(info.lastUpdated) or nil,
|
|
acquiredAt = tonumber(info.acquiredAt) or nil,
|
|
withMeSinceAt = tonumber(info.withMeSinceAt) or nil,
|
|
source = tostring(source or "stablemaster"),
|
|
}
|
|
end
|
|
|
|
local function MTH_PETS_IsSameLiveStateField(a, b)
|
|
if a == b then
|
|
return true
|
|
end
|
|
if a == nil or b == nil then
|
|
return false
|
|
end
|
|
return tostring(a) == tostring(b)
|
|
end
|
|
|
|
local function MTH_PETS_AreLiveStatesEqual(left, right)
|
|
if type(left) ~= "table" or type(right) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local keys = {
|
|
"exists", "liveExists", "dead", "suppressed", "suppressedReason",
|
|
"id", "name", "family", "level", "happiness", "loyalty", "loyaltyLevel",
|
|
"health", "healthMax", "xp", "xpMax", "xpPercent", "lastSource", "lastUpdated", "acquiredAt", "withMeSinceAt",
|
|
}
|
|
|
|
for _, key in ipairs(keys) do
|
|
if not MTH_PETS_IsSameLiveStateField(left[key], right[key]) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_NotifyLiveSubscribers(state, source)
|
|
for key, callback in pairs(MTH_PETS_LiveStateSubscribers) do
|
|
if type(callback) == "function" then
|
|
local ok, err = pcall(callback, MTH_PETS_CopyLiveState(state), source)
|
|
if not ok and MTH and MTH.Print then
|
|
MTH:Print("[PETS] live-state subscriber error ('" .. tostring(key) .. "'): " .. tostring(err), "error")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function MTH_PETS_EmitLiveState(source, force, options)
|
|
local emittedSource = source or "stablemaster"
|
|
if type(emittedSource) ~= "string" then
|
|
emittedSource = tostring(emittedSource)
|
|
end
|
|
local skipRefresh = false
|
|
if type(options) == "table" and options.skipRefresh == true then
|
|
skipRefresh = true
|
|
end
|
|
local nextState = MTH_PETS_BuildLiveState(emittedSource, skipRefresh)
|
|
local changed = force and true or (not MTH_PETS_AreLiveStatesEqual(MTH_PETS_LiveState, nextState))
|
|
if not changed then
|
|
return false
|
|
end
|
|
|
|
MTH_PETS_LiveStateSeq = MTH_PETS_LiveStateSeq + 1
|
|
nextState.seq = MTH_PETS_LiveStateSeq
|
|
nextState.changedAt = time()
|
|
MTH_PETS_LiveState = nextState
|
|
|
|
if MTH and MTH.FireEvent then
|
|
MTH:FireEvent(MTH_PETS_LiveStateEventName, MTH_PETS_CopyLiveState(nextState), emittedSource)
|
|
end
|
|
MTH_PETS_NotifyLiveSubscribers(nextState, emittedSource)
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_GetLiveState()
|
|
if type(MTH_PETS_LiveState) ~= "table" then
|
|
MTH_PETS_EmitLiveState("api:init", true)
|
|
end
|
|
return MTH_PETS_CopyLiveState(MTH_PETS_LiveState)
|
|
end
|
|
|
|
function MTH_PETS_SubscribeLiveState(key, callback, emitCurrent)
|
|
if type(key) ~= "string" or key == "" then
|
|
return false, "invalid key"
|
|
end
|
|
if type(callback) ~= "function" then
|
|
return false, "callback must be function"
|
|
end
|
|
|
|
MTH_PETS_LiveStateSubscribers[key] = callback
|
|
if emitCurrent then
|
|
local current = MTH_PETS_GetLiveState()
|
|
if type(current) == "table" then
|
|
pcall(callback, current, "subscribe:init")
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_UnsubscribeLiveState(key)
|
|
if type(key) ~= "string" or key == "" then
|
|
return false
|
|
end
|
|
MTH_PETS_LiveStateSubscribers[key] = nil
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_SafeLower(text)
|
|
if text == nil then return "" end
|
|
return string.lower(tostring(text))
|
|
end
|
|
|
|
local function MTH_PETS_NormalizeText(text)
|
|
local normalized = tostring(text or "")
|
|
normalized = string.gsub(normalized, "^%s+", "")
|
|
normalized = string.gsub(normalized, "%s+$", "")
|
|
return normalized
|
|
end
|
|
|
|
local function MTH_PETS_GetLoyaltyLevelNames()
|
|
local defaults = {
|
|
[1] = "Rebellious",
|
|
[2] = "Unruly",
|
|
[3] = "Submissive",
|
|
[4] = "Dependable",
|
|
[5] = "Faithful",
|
|
[6] = "Best Friend",
|
|
}
|
|
|
|
local names = {}
|
|
for level = 1, 6 do
|
|
local key = "PET_LOYALTY" .. tostring(level)
|
|
local value = (_G and _G[key]) or (type(getglobal) == "function" and getglobal(key))
|
|
value = MTH_PETS_NormalizeText(value)
|
|
if value ~= "" then
|
|
names[level] = value
|
|
else
|
|
names[level] = defaults[level]
|
|
end
|
|
end
|
|
|
|
return names
|
|
end
|
|
|
|
local function MTH_PETS_GetLoyaltyNameByLevel(level)
|
|
local resolvedLevel = tonumber(level)
|
|
if not resolvedLevel or resolvedLevel < 1 or resolvedLevel > 6 then
|
|
return nil
|
|
end
|
|
local names = MTH_PETS_GetLoyaltyLevelNames()
|
|
return names[resolvedLevel]
|
|
end
|
|
|
|
local function MTH_PETS_ParseLoyaltyLevelFromText(text)
|
|
text = MTH_PETS_NormalizeText(text)
|
|
if text == "" then
|
|
return nil
|
|
end
|
|
|
|
local _, _, levelText = string.find(text, "[Ll]oyalty%s*[Ll]evel%s*(%d+)")
|
|
local level = tonumber(levelText)
|
|
if level and level >= 1 and level <= 6 then
|
|
return level
|
|
end
|
|
|
|
local lowerText = string.lower(text)
|
|
local names = MTH_PETS_GetLoyaltyLevelNames()
|
|
for index = 1, 6 do
|
|
local loyaltyName = MTH_PETS_NormalizeText(names[index])
|
|
if loyaltyName ~= "" and string.find(lowerText, string.lower(loyaltyName), 1, true) then
|
|
return index
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_FormatLoyaltyDisplay(level, text)
|
|
local resolvedLevel = tonumber(level)
|
|
if (not resolvedLevel or resolvedLevel < 1 or resolvedLevel > 6) then
|
|
resolvedLevel = MTH_PETS_ParseLoyaltyLevelFromText(text)
|
|
end
|
|
local resolvedName = MTH_PETS_GetLoyaltyNameByLevel(resolvedLevel)
|
|
if resolvedLevel and resolvedName ~= "" then
|
|
return "Level " .. tostring(resolvedLevel) .. " " .. tostring(resolvedName)
|
|
end
|
|
if resolvedLevel then
|
|
return "Level " .. tostring(resolvedLevel)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_IsPlaceholderPetName(name)
|
|
local normalized = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(name))
|
|
if normalized == "" then
|
|
return true
|
|
end
|
|
if normalized == "unknown" then
|
|
return true
|
|
end
|
|
local unknownObject = _G and _G["UNKNOWNOBJECT"]
|
|
if type(unknownObject) == "string" and unknownObject ~= "" then
|
|
local unknownNormalized = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(unknownObject))
|
|
if unknownNormalized ~= "" and normalized == unknownNormalized then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_GetPlayerCoords()
|
|
if type(GetPlayerMapPosition) ~= "function" then
|
|
return nil, nil
|
|
end
|
|
local x, y = GetPlayerMapPosition("player")
|
|
x = tonumber(x)
|
|
y = tonumber(y)
|
|
if not x or not y then
|
|
return nil, nil
|
|
end
|
|
if x <= 0 and y <= 0 then
|
|
return nil, nil
|
|
end
|
|
return x, y
|
|
end
|
|
|
|
local function MTH_PETS_CaptureContext()
|
|
local x, y = MTH_PETS_GetPlayerCoords()
|
|
local zoneName = (type(GetRealZoneText) == "function") and GetRealZoneText() or nil
|
|
if (not zoneName or zoneName == "") and type(GetZoneText) == "function" then
|
|
zoneName = GetZoneText()
|
|
end
|
|
local subZoneName = (type(GetSubZoneText) == "function") and GetSubZoneText() or nil
|
|
if subZoneName == "" then
|
|
subZoneName = nil
|
|
end
|
|
if zoneName == "" then
|
|
zoneName = nil
|
|
end
|
|
local hunterLevel = (type(UnitLevel) == "function") and UnitLevel("player") or nil
|
|
return {
|
|
timestamp = time(),
|
|
zone = zoneName,
|
|
subZone = subZoneName,
|
|
x = x,
|
|
y = y,
|
|
hunterLevel = hunterLevel,
|
|
}
|
|
end
|
|
|
|
local function MTH_PETS_GetStableMasterName()
|
|
if type(UnitName) ~= "function" then
|
|
return nil
|
|
end
|
|
local name = nil
|
|
if type(UnitExists) == "function" and UnitExists("npc") then
|
|
name = UnitName("npc")
|
|
end
|
|
if (not name or name == "") and type(UnitExists) == "function" and UnitExists("target") then
|
|
if type(UnitIsPlayer) ~= "function" or not UnitIsPlayer("target") then
|
|
name = UnitName("target")
|
|
end
|
|
end
|
|
name = MTH_PETS_NormalizeText(name)
|
|
if name == "" then
|
|
return nil
|
|
end
|
|
return name
|
|
end
|
|
|
|
local function MTH_PETS_NormalizeSystemMessage(rawMessage)
|
|
local message = tostring(rawMessage or "")
|
|
if message == "" then
|
|
return ""
|
|
end
|
|
message = string.gsub(message, "|c%x%x%x%x%x%x%x%x", "")
|
|
message = string.gsub(message, "|r", "")
|
|
message = string.gsub(message, "^%s+", "")
|
|
message = string.gsub(message, "%s+$", "")
|
|
return message
|
|
end
|
|
|
|
local function MTH_PETS_NormalizeSystemMessageKey(rawMessage)
|
|
local message = MTH_PETS_NormalizeSystemMessage(rawMessage)
|
|
message = string.lower(tostring(message or ""))
|
|
message = string.gsub(message, "%%s", " ")
|
|
message = string.gsub(message, "[^%w%s]", " ")
|
|
message = string.gsub(message, "%s+", " ")
|
|
message = string.gsub(message, "^%s+", "")
|
|
message = string.gsub(message, "%s+$", "")
|
|
return message
|
|
end
|
|
|
|
local function MTH_PETS_IsRunawaySystemMessage(rawMessage)
|
|
local message = MTH_PETS_NormalizeSystemMessage(rawMessage)
|
|
if message == "" then
|
|
return false
|
|
end
|
|
|
|
local lostPetMessage = _G and _G["PETTAME_LOSTPET"]
|
|
if type(lostPetMessage) == "string" and lostPetMessage ~= "" then
|
|
if message == lostPetMessage then
|
|
return true
|
|
end
|
|
local messageKey = MTH_PETS_NormalizeSystemMessageKey(message)
|
|
local lostKey = MTH_PETS_NormalizeSystemMessageKey(lostPetMessage)
|
|
if messageKey ~= "" and lostKey ~= "" then
|
|
if messageKey == lostKey then
|
|
return true
|
|
end
|
|
if string.find(messageKey, lostKey, 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
local lower = string.lower(message)
|
|
if string.find(lower, "pet has run away", 1, true) then return true end
|
|
if string.find(lower, "pet ran away", 1, true) then return true end
|
|
if string.find(lower, "pet has fled", 1, true) then return true end
|
|
if string.find(lower, "pet fled", 1, true) then return true end
|
|
if string.find(lower, "pet has left", 1, true) then return true end
|
|
if string.find(lower, "pet left", 1, true) then return true end
|
|
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_ParseCreatureIdFromGuid(guid)
|
|
local guidText = tostring(guid or "")
|
|
if guidText == "" then
|
|
return nil
|
|
end
|
|
|
|
local _, _, creatureIdText = string.find(guidText, "^Creature%-%d+%-%d+%-%d+%-%d+%-(%d+)%-%d+$")
|
|
if creatureIdText then
|
|
return tonumber(creatureIdText)
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_ParseBeastLevelBounds(levelField)
|
|
local levelText = tostring(levelField or "")
|
|
if levelText == "" then
|
|
return nil, nil
|
|
end
|
|
local exact = tonumber(levelText)
|
|
if exact then
|
|
return exact, exact
|
|
end
|
|
local _, _, minLevel, maxLevel = string.find(levelText, "(%d+)%s*%-%s*(%d+)")
|
|
if minLevel and maxLevel then
|
|
return tonumber(minLevel), tonumber(maxLevel)
|
|
end
|
|
local _, _, single = string.find(levelText, "(%d+)")
|
|
if single then
|
|
local parsed = tonumber(single)
|
|
return parsed, parsed
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function MTH_PETS_LevelMatchesBeast(levelField, targetLevel)
|
|
local numericTargetLevel = tonumber(targetLevel)
|
|
if not numericTargetLevel then
|
|
return false
|
|
end
|
|
local minLevel, maxLevel = MTH_PETS_ParseBeastLevelBounds(levelField)
|
|
if not minLevel and not maxLevel then
|
|
return false
|
|
end
|
|
if minLevel and maxLevel then
|
|
return numericTargetLevel >= minLevel and numericTargetLevel <= maxLevel
|
|
end
|
|
if minLevel then
|
|
return numericTargetLevel == minLevel
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_FindBeastIdByDataset(name, family, level)
|
|
if type(MTH_DS_Beasts) ~= "table" then
|
|
return nil
|
|
end
|
|
local normalizedName = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(name))
|
|
if normalizedName == "" then
|
|
return nil
|
|
end
|
|
local normalizedFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(family))
|
|
local candidates = {}
|
|
for beastId, beast in pairs(MTH_DS_Beasts) do
|
|
if type(beast) == "table" then
|
|
local beastName = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(beast.name))
|
|
if beastName == normalizedName then
|
|
table.insert(candidates, { id = tonumber(beastId), beast = beast })
|
|
end
|
|
end
|
|
end
|
|
if table.getn(candidates) == 1 then
|
|
return candidates[1].id
|
|
end
|
|
if table.getn(candidates) <= 0 then
|
|
return nil
|
|
end
|
|
|
|
local familyMatches = {}
|
|
if normalizedFamily ~= "" then
|
|
for i = 1, table.getn(candidates) do
|
|
local candidate = candidates[i]
|
|
local beastFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(candidate.beast and candidate.beast.family))
|
|
if beastFamily ~= "" and beastFamily == normalizedFamily then
|
|
table.insert(familyMatches, candidate)
|
|
end
|
|
end
|
|
end
|
|
if table.getn(familyMatches) == 1 then
|
|
return familyMatches[1].id
|
|
end
|
|
|
|
local levelMatches = {}
|
|
local scanPool = candidates
|
|
if table.getn(familyMatches) > 0 then
|
|
scanPool = familyMatches
|
|
end
|
|
for i = 1, table.getn(scanPool) do
|
|
local candidate = scanPool[i]
|
|
if MTH_PETS_LevelMatchesBeast(candidate.beast and candidate.beast.lvl, level) then
|
|
table.insert(levelMatches, candidate)
|
|
end
|
|
end
|
|
if table.getn(levelMatches) == 1 then
|
|
return levelMatches[1].id
|
|
end
|
|
|
|
if table.getn(scanPool) > 0 then
|
|
return scanPool[1].id
|
|
end
|
|
return candidates[1].id
|
|
end
|
|
|
|
local function MTH_PETS_CaptureTargetSnapshot()
|
|
if type(UnitExists) ~= "function" or not UnitExists("target") then
|
|
return nil
|
|
end
|
|
local targetName = (type(UnitName) == "function") and UnitName("target") or nil
|
|
if MTH_PETS_IsPlaceholderPetName(targetName) then
|
|
return nil
|
|
end
|
|
local targetGuid = (type(UnitGUID) == "function") and UnitGUID("target") or nil
|
|
local beastId = MTH_PETS_ParseCreatureIdFromGuid(targetGuid)
|
|
local targetFamily = (type(UnitCreatureFamily) == "function") and UnitCreatureFamily("target") or nil
|
|
local targetLevel = (type(UnitLevel) == "function") and UnitLevel("target") or nil
|
|
if beastId == nil then
|
|
beastId = MTH_PETS_FindBeastIdByDataset(targetName, targetFamily, targetLevel)
|
|
end
|
|
return {
|
|
capturedAt = time(),
|
|
name = targetName,
|
|
family = targetFamily,
|
|
level = targetLevel,
|
|
guid = targetGuid,
|
|
beastId = beastId,
|
|
}
|
|
end
|
|
|
|
local function MTH_PETS_IsTameBeastSpellName(spellName)
|
|
local normalized = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(spellName))
|
|
if normalized == "" then
|
|
return false
|
|
end
|
|
if normalized == "tame beast" then
|
|
return true
|
|
end
|
|
local globalTameName = _G and _G["TAMEBEAST"] or nil
|
|
if type(globalTameName) == "string" then
|
|
local tameNormalized = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(globalTameName))
|
|
if tameNormalized ~= "" then
|
|
if normalized == tameNormalized then
|
|
return true
|
|
end
|
|
if string.find(normalized, tameNormalized, 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
if string.find(normalized, "tame beast", 1, true) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_GetCurrentPlayerCastOrChannelName()
|
|
if type(UnitChannelInfo) == "function" then
|
|
local channelName = UnitChannelInfo("player")
|
|
if channelName and tostring(channelName) ~= "" then
|
|
return tostring(channelName), "UnitChannelInfo"
|
|
end
|
|
end
|
|
if type(UnitCastingInfo) == "function" then
|
|
local castName = UnitCastingInfo("player")
|
|
if castName and tostring(castName) ~= "" then
|
|
return tostring(castName), "UnitCastingInfo"
|
|
end
|
|
end
|
|
local castingBarText = _G and _G["CastingBarText"]
|
|
if castingBarText and type(castingBarText.GetText) == "function" then
|
|
local barText = castingBarText:GetText()
|
|
if barText and tostring(barText) ~= "" then
|
|
return tostring(barText), "CastingBarText"
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function MTH_PETS_IsTargetBeastCandidate()
|
|
if type(UnitExists) ~= "function" or not UnitExists("target") then
|
|
return false
|
|
end
|
|
if type(UnitIsPlayer) == "function" and UnitIsPlayer("target") then
|
|
return false
|
|
end
|
|
if type(UnitCreatureType) == "function" then
|
|
local creatureType = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(UnitCreatureType("target")))
|
|
if creatureType ~= "" and creatureType ~= "beast" then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_RecordTameAttempt(source, spellName)
|
|
MTH_PETS_LogTame("Channel start source=" .. tostring(source) .. " spell='" .. tostring(spellName or "") .. "'")
|
|
local resolvedSpellName = spellName
|
|
if not MTH_PETS_IsTameBeastSpellName(resolvedSpellName) then
|
|
local numericArg = tonumber(resolvedSpellName)
|
|
if numericArg and type(GetSpellInfo) == "function" then
|
|
local byIdName = GetSpellInfo(numericArg)
|
|
if byIdName and MTH_PETS_IsTameBeastSpellName(byIdName) then
|
|
resolvedSpellName = byIdName
|
|
MTH_PETS_LogTame("Resolved channel spell via GetSpellInfo(" .. tostring(numericArg) .. ") => '" .. tostring(byIdName) .. "'")
|
|
end
|
|
end
|
|
end
|
|
if not MTH_PETS_IsTameBeastSpellName(resolvedSpellName) then
|
|
local fallbackName, fallbackSource = MTH_PETS_GetCurrentPlayerCastOrChannelName()
|
|
if fallbackName and MTH_PETS_IsTameBeastSpellName(fallbackName) then
|
|
resolvedSpellName = fallbackName
|
|
MTH_PETS_LogTame("Resolved channel spell via " .. tostring(fallbackSource) .. " => '" .. tostring(fallbackName) .. "'")
|
|
end
|
|
end
|
|
if not MTH_PETS_IsTameBeastSpellName(resolvedSpellName) then
|
|
local numericArg = tonumber(spellName)
|
|
local hasNoPet = (type(UnitExists) ~= "function") or (not UnitExists("pet"))
|
|
if numericArg and numericArg >= 18000 and numericArg <= 22000 and hasNoPet and MTH_PETS_IsTargetBeastCandidate() then
|
|
local tameLabel = (_G and _G["TAMEBEAST"]) or "Tame Beast"
|
|
resolvedSpellName = tameLabel
|
|
MTH_PETS_LogTame("Resolved channel as Tame Beast via duration heuristic arg=" .. tostring(numericArg))
|
|
end
|
|
end
|
|
if not MTH_PETS_IsTameBeastSpellName(resolvedSpellName) then
|
|
MTH_PETS_LogTame("Ignored channel start: not Tame Beast")
|
|
local fallbackName, fallbackSource = MTH_PETS_GetCurrentPlayerCastOrChannelName()
|
|
if fallbackName then
|
|
MTH_PETS_LogTame("Fallback spell from " .. tostring(fallbackSource) .. " was '" .. tostring(fallbackName) .. "'")
|
|
end
|
|
return false
|
|
end
|
|
local targetSnapshot = MTH_PETS_CaptureTargetSnapshot()
|
|
if type(targetSnapshot) ~= "table" then
|
|
MTH_PETS_LogTame("Failed capture: no valid target snapshot")
|
|
return false
|
|
end
|
|
MTH_PETS_LastTameAttempt = {
|
|
source = tostring(source or "unknown"),
|
|
spellName = tostring(resolvedSpellName or ""),
|
|
capturedAt = targetSnapshot.capturedAt,
|
|
target = targetSnapshot,
|
|
}
|
|
MTH_PETS_LogTame("Captured target name='" .. tostring(targetSnapshot.name or "")
|
|
.. "' family='" .. tostring(targetSnapshot.family or "")
|
|
.. "' level='" .. tostring(targetSnapshot.level or "")
|
|
.. "' guid='" .. tostring(targetSnapshot.guid or "")
|
|
.. "' beastId='" .. tostring(targetSnapshot.beastId or "") .. "'")
|
|
MTH_PETS_LogConsistency("TameAttempt captured source=" .. tostring(source)
|
|
.. " target='" .. tostring(targetSnapshot.name or "") .. "'"
|
|
.. " family='" .. tostring(targetSnapshot.family or "") .. "'"
|
|
.. " level='" .. tostring(targetSnapshot.level or "") .. "'"
|
|
.. " beastId='" .. tostring(targetSnapshot.beastId or "") .. "'")
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_GetPendingTameBeastId(snapshot)
|
|
local pending = MTH_PETS_LastTameAttempt
|
|
if type(pending) ~= "table" or type(pending.target) ~= "table" then
|
|
MTH_PETS_LogTame("Resolve beastId: no pending tame attempt")
|
|
return nil
|
|
end
|
|
local pendingAt = tonumber(pending.capturedAt) or 0
|
|
if pendingAt <= 0 or (time() - pendingAt) > 45 then
|
|
MTH_PETS_LogTame("Resolve beastId: pending tame expired age=" .. tostring((time() - pendingAt)))
|
|
return nil
|
|
end
|
|
local pendingTarget = pending.target
|
|
if type(snapshot) == "table" then
|
|
local targetFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(pendingTarget.family))
|
|
local petFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(snapshot.family))
|
|
if targetFamily ~= "" and petFamily ~= "" and targetFamily ~= petFamily then
|
|
MTH_PETS_LogTame("Resolve beastId: family mismatch target='" .. tostring(targetFamily) .. "' pet='" .. tostring(petFamily) .. "'")
|
|
return nil
|
|
end
|
|
local targetLevel = tonumber(pendingTarget.level)
|
|
local petLevel = tonumber(snapshot.level)
|
|
if targetLevel and petLevel and targetLevel ~= petLevel then
|
|
MTH_PETS_LogTame("Resolve beastId: level mismatch target='" .. tostring(targetLevel) .. "' pet='" .. tostring(petLevel) .. "'")
|
|
return nil
|
|
end
|
|
end
|
|
if tonumber(pendingTarget.beastId) and tonumber(pendingTarget.beastId) > 0 then
|
|
MTH_PETS_LogTame("Resolve beastId: using captured beastId=" .. tostring(pendingTarget.beastId))
|
|
return tonumber(pendingTarget.beastId)
|
|
end
|
|
local fromTarget = MTH_PETS_FindBeastIdByDataset(pendingTarget.name, pendingTarget.family, pendingTarget.level)
|
|
if fromTarget then
|
|
MTH_PETS_LogTame("Resolve beastId: DS lookup matched beastId=" .. tostring(fromTarget))
|
|
return fromTarget
|
|
end
|
|
MTH_PETS_LogTame("Resolve beastId: DS lookup failed for target='" .. tostring(pendingTarget.name or "") .. "'")
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_ResolveTameBeastIdForRow(row, snapshot)
|
|
if type(row) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local pendingResolved = MTH_PETS_GetPendingTameBeastId(snapshot)
|
|
if pendingResolved and pendingResolved > 0 then
|
|
return pendingResolved
|
|
end
|
|
|
|
local fromSnapshot = snapshot and tonumber(snapshot.beastId) or nil
|
|
if fromSnapshot and fromSnapshot > 0 then
|
|
return fromSnapshot
|
|
end
|
|
|
|
local fromRow = tonumber(row.beastId)
|
|
if fromRow and fromRow > 0 then
|
|
return fromRow
|
|
end
|
|
|
|
local lookup = MTH_PETS_FindBeastIdByDataset(
|
|
tostring((snapshot and snapshot.name) or row.name or ""),
|
|
tostring((snapshot and snapshot.family) or row.family or ""),
|
|
tonumber((snapshot and snapshot.level) or row.level)
|
|
)
|
|
if lookup and lookup > 0 then
|
|
return lookup
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_MakeSignature(name, family, 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)
|
|
end
|
|
|
|
local function MTH_PETS_RowHasTameRecord(row)
|
|
if type(row) ~= "table" then
|
|
return false
|
|
end
|
|
if row.tameRecorded == true then
|
|
return true
|
|
end
|
|
if tonumber(row.tamedAt) and tonumber(row.tamedAt) > 0 then
|
|
return true
|
|
end
|
|
if tonumber(row.tameBeastId) and tonumber(row.tameBeastId) > 0 then
|
|
return true
|
|
end
|
|
if type(row.tameZone) == "string" and row.tameZone ~= "" then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PETS_BackfillTameMetadataFromEvents(row)
|
|
if type(row) ~= "table" then
|
|
return false
|
|
end
|
|
if MTH_PETS_RowHasTameRecord(row) then
|
|
return false
|
|
end
|
|
if type(row.events) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local bestEvent = nil
|
|
local stableFirstSeenAt = tonumber(row.stableFirstSeenAt) or tonumber(row.stabledAt) or 0
|
|
for i = 1, table.getn(row.events) do
|
|
local ev = row.events[i]
|
|
if type(ev) == "table" and tostring(ev.type or "") == "pet-acquired" then
|
|
local evAt = tonumber(ev.at) or 0
|
|
local timelineOk = true
|
|
if stableFirstSeenAt > 0 and evAt > 0 and evAt > stableFirstSeenAt then
|
|
-- If we only saw this pet as stabled before the acquire event, it was likely a call-out, not a tame.
|
|
timelineOk = false
|
|
end
|
|
if timelineOk then
|
|
if not bestEvent then
|
|
bestEvent = ev
|
|
else
|
|
local bestAt = tonumber(bestEvent.at) or 0
|
|
if bestAt <= 0 or (evAt > 0 and evAt < bestAt) then
|
|
bestEvent = ev
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if type(bestEvent) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local context = type(bestEvent.context) == "table" and bestEvent.context or nil
|
|
local changed = false
|
|
|
|
if row.tamedAt == nil or tonumber(row.tamedAt) == nil or tonumber(row.tamedAt) <= 0 then
|
|
local candidateAt = tonumber(bestEvent.at) or (context and tonumber(context.timestamp) or nil) or tonumber(row.createdAt)
|
|
if candidateAt and candidateAt > 0 then
|
|
row.tamedAt = candidateAt
|
|
changed = true
|
|
end
|
|
end
|
|
|
|
if context then
|
|
if row.tameHunterLevel == nil and context.hunterLevel ~= nil then
|
|
row.tameHunterLevel = context.hunterLevel
|
|
changed = true
|
|
end
|
|
if (row.tameZone == nil or row.tameZone == "") and context.zone ~= nil and tostring(context.zone) ~= "" then
|
|
row.tameZone = context.zone
|
|
changed = true
|
|
end
|
|
if (row.tameSubZone == nil or row.tameSubZone == "") and context.subZone ~= nil and tostring(context.subZone) ~= "" then
|
|
row.tameSubZone = context.subZone
|
|
changed = true
|
|
end
|
|
if row.tameX == nil and context.x ~= nil then
|
|
row.tameX = context.x
|
|
changed = true
|
|
end
|
|
if row.tameY == nil and context.y ~= nil then
|
|
row.tameY = context.y
|
|
changed = true
|
|
end
|
|
end
|
|
|
|
if (row.tameBeastId == nil or tonumber(row.tameBeastId) == nil) and row.beastId ~= nil then
|
|
local beastId = tonumber(row.beastId)
|
|
if beastId and beastId > 0 then
|
|
row.tameBeastId = beastId
|
|
changed = true
|
|
end
|
|
end
|
|
|
|
if changed then
|
|
row.tameRecorded = true
|
|
row.lastUpdated = time()
|
|
end
|
|
|
|
return changed
|
|
end
|
|
|
|
local function MTH_PETS_EnsurePetStoreSchema(pets)
|
|
if type(pets.petStore) ~= "table" then
|
|
pets.petStore = {}
|
|
end
|
|
local petStore = pets.petStore
|
|
if type(petStore.activeById) ~= "table" then
|
|
petStore.activeById = {}
|
|
end
|
|
if type(petStore.historyById) ~= "table" then
|
|
petStore.historyById = {}
|
|
end
|
|
if type(petStore.signatureIndex) ~= "table" then
|
|
petStore.signatureIndex = {}
|
|
end
|
|
if type(petStore.guidIndex) ~= "table" then
|
|
petStore.guidIndex = {}
|
|
end
|
|
if type(petStore.stableSlotIndex) ~= "table" then
|
|
petStore.stableSlotIndex = {}
|
|
end
|
|
if tonumber(petStore.nextId) == nil or tonumber(petStore.nextId) < 1 then
|
|
petStore.nextId = 1
|
|
end
|
|
if petStore.activeCurrentId == nil then
|
|
petStore.activeCurrentId = nil
|
|
end
|
|
|
|
for _, row in pairs(petStore.activeById) do
|
|
if type(row) == "table" then
|
|
if row.loyaltyLevel == nil and type(row.loyalty) == "string" then
|
|
row.loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(row.loyalty)
|
|
end
|
|
if type(row.loyalty) == "string" then
|
|
row.loyalty = nil
|
|
end
|
|
row.loyaltyText = nil
|
|
row.loyaltyDisplay = nil
|
|
if type(row.stableInfo) == "table" then
|
|
if row.stableInfo.loyaltyLevel == nil then
|
|
row.stableInfo.loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(row.stableInfo.loyalty)
|
|
end
|
|
row.stableInfo.loyalty = nil
|
|
end
|
|
MTH_PETS_BackfillTameMetadataFromEvents(row)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
if type(pets.currentPet) ~= "table" then
|
|
pets.currentPet = {}
|
|
end
|
|
local currentPet = pets.currentPet
|
|
if currentPet.id == nil then currentPet.id = nil end
|
|
if currentPet.guid == nil then currentPet.guid = nil end
|
|
if currentPet.signature == nil then currentPet.signature = nil end
|
|
if currentPet.beastId == nil then currentPet.beastId = nil end
|
|
if currentPet.name == nil then currentPet.name = nil end
|
|
if currentPet.family == nil then currentPet.family = nil end
|
|
if currentPet.level == nil then currentPet.level = nil end
|
|
if currentPet.happiness == nil then currentPet.happiness = nil end
|
|
if currentPet.loyalty == nil then currentPet.loyalty = nil end
|
|
if currentPet.loyaltyLevel == nil then currentPet.loyaltyLevel = nil end
|
|
if currentPet.loyaltyLevel == nil and type(currentPet.loyalty) == "string" then
|
|
currentPet.loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(currentPet.loyalty)
|
|
end
|
|
if type(currentPet.loyalty) == "string" then
|
|
currentPet.loyalty = nil
|
|
end
|
|
currentPet.loyaltyText = nil
|
|
currentPet.loyaltyDisplay = nil
|
|
if currentPet.xp == nil then currentPet.xp = nil end
|
|
if currentPet.xpMax == nil then currentPet.xpMax = nil end
|
|
if currentPet.xpPercent == nil then currentPet.xpPercent = nil end
|
|
if currentPet.exists == nil then currentPet.exists = false end
|
|
if currentPet.lastSeen == nil then currentPet.lastSeen = 0 end
|
|
if currentPet.lastUpdated == nil then currentPet.lastUpdated = 0 end
|
|
if currentPet.acquiredAt == nil then currentPet.acquiredAt = 0 end
|
|
if currentPet.lastSource == nil then currentPet.lastSource = "" end
|
|
end
|
|
|
|
local function MTH_PETS_NormalizePetId(petId)
|
|
if petId == nil then
|
|
return nil
|
|
end
|
|
local value = tostring(petId)
|
|
if value == "" then
|
|
return nil
|
|
end
|
|
return value
|
|
end
|
|
|
|
local function MTH_PETS_SetCurrentPetId(pets, petId)
|
|
if type(pets) ~= "table" then
|
|
return nil
|
|
end
|
|
local normalized = MTH_PETS_NormalizePetId(petId)
|
|
pets.currentPetId = normalized
|
|
if type(pets.petStore) == "table" then
|
|
pets.petStore.activeCurrentId = normalized
|
|
end
|
|
return normalized
|
|
end
|
|
|
|
local function MTH_PETS_SetCurrentPetSuppressed(pets, suppressed, reason)
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
pets.currentPetSuppressed = suppressed and true or false
|
|
pets.currentPetSuppressedAwaitNoLive = suppressed and true or false
|
|
pets.currentPetSuppressedReason = suppressed and tostring(reason or "") or ""
|
|
pets.currentPetSuppressedAt = time()
|
|
end
|
|
|
|
local function MTH_PETS_ResolveNoLiveCurrentRow(pets, previousCurrentId)
|
|
if type(pets) ~= "table" then
|
|
return nil, nil
|
|
end
|
|
if pets.currentPetSuppressed == true then
|
|
return nil, nil
|
|
end
|
|
local petStore = (type(pets.petStore) == "table") and pets.petStore or nil
|
|
if type(petStore) ~= "table" or type(petStore.activeById) ~= "table" then
|
|
return nil, nil
|
|
end
|
|
|
|
local cp = pets.currentPet
|
|
local candidates = {
|
|
previousCurrentId,
|
|
cp and cp.id or nil,
|
|
pets.currentPetId,
|
|
petStore.activeCurrentId,
|
|
}
|
|
|
|
local seen = {}
|
|
for i = 1, table.getn(candidates) do
|
|
local candidateId = candidates[i]
|
|
if candidateId ~= nil then
|
|
local key = tostring(candidateId)
|
|
if not seen[key] then
|
|
seen[key] = true
|
|
local row = petStore.activeById[candidateId]
|
|
if type(row) == "table" then
|
|
local stableSlot = tonumber(row.stableSlot)
|
|
if stableSlot and stableSlot > 0 then
|
|
return nil, nil
|
|
end
|
|
return tostring(candidateId), row
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
local function MTH_PETS_ApplyCurrentPetFromActiveRow(cp, petId, row)
|
|
if type(cp) ~= "table" or petId == nil or type(row) ~= "table" then
|
|
return
|
|
end
|
|
cp.id = tostring(petId)
|
|
cp.guid = row.guid
|
|
cp.signature = row.signature
|
|
cp.beastId = row.beastId
|
|
cp.name = row.name
|
|
cp.family = row.family
|
|
cp.level = row.level
|
|
cp.happiness = row.happiness
|
|
cp.loyalty = row.loyalty
|
|
cp.loyaltyLevel = row.loyaltyLevel
|
|
cp.xp = row.xp
|
|
cp.xpMax = row.xpMax
|
|
cp.xpPercent = row.xpPercent
|
|
if cp.acquiredAt == nil or tonumber(cp.acquiredAt) == nil or tonumber(cp.acquiredAt) <= 0 then
|
|
cp.acquiredAt = tonumber(row.createdAt) or tonumber(row.lastSeen) or tonumber(row.lastUpdated) or time()
|
|
end
|
|
if tonumber(cp.lastSeen) == nil or tonumber(cp.lastSeen) <= 0 then
|
|
cp.lastSeen = tonumber(row.lastSeen) or tonumber(row.lastUpdated) or time()
|
|
end
|
|
end
|
|
|
|
local function MTH_PETS_EnsureSchema(pets)
|
|
local now = time()
|
|
if pets.schemaVersion ~= MTH_PETS_SCHEMA_VERSION then
|
|
pets.schemaVersion = MTH_PETS_SCHEMA_VERSION
|
|
pets.schemaMigratedAt = now
|
|
if type(pets.schemaMigration) ~= "table" then
|
|
pets.schemaMigration = {}
|
|
end
|
|
pets.schemaMigration.version = MTH_PETS_SCHEMA_VERSION
|
|
pets.schemaMigration.at = now
|
|
pets.schemaMigration.note = "beta canonical store"
|
|
end
|
|
if pets.updatedAt == nil then
|
|
pets.updatedAt = 0
|
|
end
|
|
if pets.createdAt == nil or tonumber(pets.createdAt) == nil or tonumber(pets.createdAt) <= 0 then
|
|
pets.createdAt = now
|
|
end
|
|
if pets.currentPetId == nil then
|
|
local cpId = (type(pets.currentPet) == "table") and pets.currentPet.id or nil
|
|
pets.currentPetId = MTH_PETS_NormalizePetId(cpId)
|
|
end
|
|
if pets.currentPetSuppressed == nil then
|
|
pets.currentPetSuppressed = false
|
|
end
|
|
if pets.currentPetSuppressedAwaitNoLive == nil then
|
|
pets.currentPetSuppressedAwaitNoLive = false
|
|
end
|
|
if pets.currentPetSuppressedReason == nil then
|
|
pets.currentPetSuppressedReason = ""
|
|
end
|
|
if pets.hasVisitedStableOnce == nil then
|
|
pets.hasVisitedStableOnce = false
|
|
end
|
|
if pets.stableVisitPromptLastAt == nil then
|
|
pets.stableVisitPromptLastAt = 0
|
|
end
|
|
MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
MTH_PETS_EnsurePetStoreSchema(pets)
|
|
end
|
|
|
|
local function MTH_PETS_MarkStableVisited(pets, source)
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
if pets.hasVisitedStableOnce == true then
|
|
return
|
|
end
|
|
pets.hasVisitedStableOnce = true
|
|
pets.stableVisitedAt = time()
|
|
pets.stableVisitedSource = source or "stable-scan"
|
|
pets.updatedAt = time()
|
|
end
|
|
|
|
local function MTH_PETS_MaybePromptStableVisit(pets)
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
if pets.hasVisitedStableOnce == true then
|
|
return
|
|
end
|
|
local playerLevel = (type(UnitLevel) == "function") and tonumber(UnitLevel("player")) or nil
|
|
if not playerLevel or playerLevel <= 12 then
|
|
return
|
|
end
|
|
local now = time()
|
|
local lastPrompt = tonumber(pets.stableVisitPromptLastAt) or 0
|
|
if lastPrompt > 0 and (now - lastPrompt) <= 60 then
|
|
return
|
|
end
|
|
pets.stableVisitPromptLastAt = now
|
|
if MTH and MTH.Print then
|
|
MTH:Print("No Stable scan found for this character. Visit a Stable Master once to record stable pets and slots.")
|
|
end
|
|
end
|
|
|
|
local function MTH_PETS_EnsurePetTrainingCompatibility(pets)
|
|
if type(pets.trainScan) ~= "table" and type(pets.petTraining) == "table" then
|
|
pets.trainScan = pets.petTraining
|
|
end
|
|
if type(pets.trainScan) ~= "table" then
|
|
pets.trainScan = {}
|
|
end
|
|
if type(pets.petTraining) ~= "table" then
|
|
pets.petTraining = pets.trainScan
|
|
end
|
|
if pets.petTraining ~= pets.trainScan then
|
|
pets.petTraining = pets.trainScan
|
|
end
|
|
local pt = pets.trainScan
|
|
if type(pt.spellMap) ~= "table" then
|
|
pt.spellMap = {}
|
|
end
|
|
if type(pt.hunterKnownMap) ~= "table" then
|
|
pt.hunterKnownMap = {}
|
|
end
|
|
if pt.lastScan == nil then
|
|
pt.lastScan = 0
|
|
end
|
|
if pt.lastPrompt == nil then
|
|
pt.lastPrompt = 0
|
|
end
|
|
|
|
if pt.hasCompletedPetScan == nil then
|
|
if (tonumber(pt.lastScan) or 0) > 0 then
|
|
pt.hasCompletedPetScan = 1
|
|
else
|
|
for _ in pairs(pt.spellMap) do
|
|
pt.hasCompletedPetScan = 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function MTH_PETS_GetStoreTables(pets)
|
|
if type(pets) ~= "table" then
|
|
return nil
|
|
end
|
|
MTH_PETS_EnsurePetStoreSchema(pets)
|
|
return pets.petStore
|
|
end
|
|
|
|
local function MTH_PETS_AddSignatureIndex(store, signature, petId)
|
|
if signature == nil or signature == "" or petId == nil then
|
|
return
|
|
end
|
|
if type(store.signatureIndex[signature]) ~= "table" then
|
|
store.signatureIndex[signature] = {}
|
|
end
|
|
local list = store.signatureIndex[signature]
|
|
for _, existingId in pairs(list) do
|
|
if existingId == petId then
|
|
return
|
|
end
|
|
end
|
|
table.insert(list, petId)
|
|
end
|
|
|
|
local function MTH_PETS_RemoveSignatureIndex(store, signature, petId)
|
|
if signature == nil or signature == "" or petId == nil then
|
|
return
|
|
end
|
|
local list = store.signatureIndex[signature]
|
|
if type(list) ~= "table" then
|
|
return
|
|
end
|
|
for i = table.getn(list), 1, -1 do
|
|
if list[i] == petId then
|
|
table.remove(list, i)
|
|
end
|
|
end
|
|
if table.getn(list) <= 0 then
|
|
store.signatureIndex[signature] = nil
|
|
end
|
|
end
|
|
|
|
local function MTH_PETS_GetPetById(pets, petId)
|
|
if petId == nil or type(pets) ~= "table" then
|
|
return nil
|
|
end
|
|
local store = MTH_PETS_GetStoreTables(pets)
|
|
if not store then
|
|
return nil
|
|
end
|
|
return store.activeById[petId]
|
|
end
|
|
|
|
local function MTH_PETS_CreatePetId(store)
|
|
local nextId = tonumber(store.nextId) or 1
|
|
store.nextId = nextId + 1
|
|
return "pet-" .. tostring(nextId)
|
|
end
|
|
|
|
local function MTH_PETS_MakeSnapshotFromLivePet()
|
|
local exists = (type(UnitExists) == "function") and UnitExists("pet")
|
|
if not exists then
|
|
return nil
|
|
end
|
|
local petName = (type(UnitName) == "function") and UnitName("pet") or nil
|
|
if MTH_PETS_IsPlaceholderPetName(petName) then
|
|
MTH_PETS_LogConsistency("MakeSnapshotFromLivePet ignored placeholder name='" .. tostring(petName or "") .. "'")
|
|
return nil
|
|
end
|
|
local petFamily = (type(UnitCreatureFamily) == "function") and UnitCreatureFamily("pet") or nil
|
|
local petLevel = (type(UnitLevel) == "function") and UnitLevel("pet") or nil
|
|
local happiness = nil
|
|
local loyalty = nil
|
|
local loyaltyLevel = nil
|
|
local xp = nil
|
|
local xpMax = nil
|
|
local xpPercent = nil
|
|
if type(GetPetHappiness) == "function" then
|
|
local h, _, l = GetPetHappiness()
|
|
happiness = h
|
|
loyalty = l
|
|
end
|
|
local getPetLoyalty = (type(getglobal) == "function" and getglobal("GetPetLoyalty")) or (_G and _G["GetPetLoyalty"])
|
|
if type(getPetLoyalty) == "function" then
|
|
local lLevel, lText = getPetLoyalty()
|
|
if lLevel ~= nil then
|
|
loyaltyLevel = tonumber(lLevel) or nil
|
|
end
|
|
if lText ~= nil and tostring(lText) ~= "" then
|
|
local lTextValue = tostring(lText)
|
|
local parsedFromText = MTH_PETS_ParseLoyaltyLevelFromText(lTextValue)
|
|
if loyaltyLevel == nil and parsedFromText ~= nil then
|
|
loyaltyLevel = parsedFromText
|
|
end
|
|
end
|
|
end
|
|
local petFrameHappiness = (_G and _G["PetFrameHappiness"]) or (type(getglobal) == "function" and getglobal("PetFrameHappiness"))
|
|
if type(petFrameHappiness) == "table" and type(petFrameHappiness.tooltipLoyalty) == "string" and petFrameHappiness.tooltipLoyalty ~= "" then
|
|
local tooltipText = tostring(petFrameHappiness.tooltipLoyalty)
|
|
local parsedFromTooltip = MTH_PETS_ParseLoyaltyLevelFromText(tooltipText)
|
|
if loyaltyLevel == nil and parsedFromTooltip ~= nil then
|
|
loyaltyLevel = parsedFromTooltip
|
|
end
|
|
end
|
|
local petLoyaltyText = (_G and _G["PetLoyaltyText"]) or (type(getglobal) == "function" and getglobal("PetLoyaltyText"))
|
|
if type(petLoyaltyText) == "table" and type(petLoyaltyText.GetText) == "function" then
|
|
local uiLoyaltyText = MTH_PETS_NormalizeText(petLoyaltyText:GetText())
|
|
if uiLoyaltyText ~= "" then
|
|
local parsedFromUi = MTH_PETS_ParseLoyaltyLevelFromText(uiLoyaltyText)
|
|
if loyaltyLevel == nil and parsedFromUi ~= nil then
|
|
loyaltyLevel = parsedFromUi
|
|
end
|
|
end
|
|
end
|
|
local getPetExperience = (type(getglobal) == "function" and getglobal("GetPetExperience")) or (_G and _G["GetPetExperience"])
|
|
if type(getPetExperience) == "function" then
|
|
local pxp, pxpMax = getPetExperience()
|
|
xp = tonumber(pxp)
|
|
xpMax = tonumber(pxpMax)
|
|
if xp and xpMax and xpMax > 0 then
|
|
xpPercent = math.floor((xp / xpMax) * 1000 + 0.5) / 10
|
|
end
|
|
end
|
|
local petGuid = (type(UnitGUID) == "function") and UnitGUID("pet") or nil
|
|
local beastId = MTH_PETS_ParseCreatureIdFromGuid(petGuid)
|
|
if beastId == nil and type(UnitGUID) == "function" then
|
|
beastId = MTH_PETS_ParseCreatureIdFromGuid(UnitGUID("target"))
|
|
end
|
|
local signature = MTH_PETS_MakeSignature(petName, petFamily, petLevel)
|
|
return {
|
|
name = petName,
|
|
family = petFamily,
|
|
level = petLevel,
|
|
happiness = happiness,
|
|
loyalty = loyalty,
|
|
loyaltyLevel = loyaltyLevel,
|
|
xp = xp,
|
|
xpMax = xpMax,
|
|
xpPercent = xpPercent,
|
|
guid = petGuid,
|
|
beastId = beastId,
|
|
signature = signature,
|
|
}
|
|
end
|
|
|
|
local function MTH_PETS_SelectPetIdBySnapshot(store, snapshot, previousPetId)
|
|
if type(store) ~= "table" or type(snapshot) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
if previousPetId and store.activeById[previousPetId] then
|
|
local previousRow = store.activeById[previousPetId]
|
|
if previousRow.guid and snapshot.guid and previousRow.guid == snapshot.guid then
|
|
return previousPetId
|
|
end
|
|
if previousRow.signature and snapshot.signature and previousRow.signature == snapshot.signature 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]
|
|
end
|
|
|
|
if snapshot.signature and snapshot.signature ~= "" then
|
|
local list = store.signatureIndex[snapshot.signature]
|
|
if type(list) == "table" then
|
|
local count = 0
|
|
local candidateId = nil
|
|
for _, listedId in pairs(list) do
|
|
if listedId and store.activeById[listedId] then
|
|
count = count + 1
|
|
candidateId = listedId
|
|
end
|
|
end
|
|
if count == 1 then
|
|
return candidateId
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_ApplySnapshotToRow(row, snapshot, source, context)
|
|
if type(row) ~= "table" or type(snapshot) ~= "table" then
|
|
return
|
|
end
|
|
local isStableScan = (source == "stable-scan")
|
|
MTH_PETS_LogConsistency("ApplySnapshotToRow(before) source=" .. tostring(source)
|
|
.. " row=" .. MTH_PETS_FormatRowConsistency(row)
|
|
.. " snapshotName='" .. tostring(snapshot.name or "") .. "'")
|
|
local now = time()
|
|
row.name = snapshot.name
|
|
row.family = snapshot.family
|
|
row.level = snapshot.level
|
|
if snapshot.happiness ~= nil then
|
|
row.happiness = snapshot.happiness
|
|
elseif not isStableScan and row.happiness == nil then
|
|
row.happiness = nil
|
|
end
|
|
if not isStableScan then
|
|
if snapshot.loyalty ~= nil then
|
|
row.loyalty = snapshot.loyalty
|
|
end
|
|
if snapshot.loyaltyLevel ~= nil then
|
|
row.loyaltyLevel = snapshot.loyaltyLevel
|
|
end
|
|
if snapshot.xp ~= nil then
|
|
row.xp = snapshot.xp
|
|
end
|
|
if snapshot.xpMax ~= nil then
|
|
row.xpMax = snapshot.xpMax
|
|
end
|
|
if snapshot.xpPercent ~= nil then
|
|
row.xpPercent = snapshot.xpPercent
|
|
end
|
|
else
|
|
if (row.loyalty == nil or tostring(row.loyalty) == "") and snapshot.loyalty ~= nil and tostring(snapshot.loyalty) ~= "" then
|
|
row.loyalty = snapshot.loyalty
|
|
end
|
|
if row.loyaltyLevel == nil and snapshot.loyaltyLevel ~= nil then
|
|
row.loyaltyLevel = snapshot.loyaltyLevel
|
|
end
|
|
end
|
|
row.guid = snapshot.guid or row.guid
|
|
row.beastId = snapshot.beastId or row.beastId
|
|
local hasPendingTame = type(MTH_PETS_LastTameAttempt) == "table"
|
|
if hasPendingTame and (row.tameBeastId == nil or tonumber(row.tameBeastId) == nil) then
|
|
local resolvedTameBeastId = MTH_PETS_ResolveTameBeastIdForRow(row, snapshot)
|
|
if resolvedTameBeastId and resolvedTameBeastId > 0 then
|
|
row.tameBeastId = resolvedTameBeastId
|
|
end
|
|
end
|
|
if hasPendingTame and (source == "unit-pet-acquire" or source == "refresh-current-pet") and type(context) == "table" then
|
|
row.tameRecorded = true
|
|
row.tamedAt = row.tamedAt or context.timestamp
|
|
row.tameHunterLevel = row.tameHunterLevel or context.hunterLevel
|
|
row.tameZone = row.tameZone or context.zone
|
|
row.tameSubZone = row.tameSubZone or context.subZone
|
|
row.tameX = row.tameX or context.x
|
|
row.tameY = row.tameY or context.y
|
|
end
|
|
row.signature = snapshot.signature
|
|
row.lastUpdated = now
|
|
if not isStableScan then
|
|
row.lastSeen = now
|
|
end
|
|
row.status = "active"
|
|
row.lastSource = source or row.lastSource
|
|
if source ~= "stable-scan" then
|
|
if row.stabledAt then
|
|
row.lastUnstabledAt = now
|
|
end
|
|
row.stabledAt = nil
|
|
end
|
|
if type(context) == "table" then
|
|
row.lastContext = context
|
|
if not isStableScan then
|
|
row.zone = context.zone or row.zone
|
|
row.subZone = context.subZone or row.subZone
|
|
row.x = context.x or row.x
|
|
row.y = context.y or row.y
|
|
row.hunterLevel = context.hunterLevel or row.hunterLevel
|
|
end
|
|
end
|
|
if not isStableScan and type(context) == "table" then
|
|
local hasTameRecord = (row.tameRecorded == true)
|
|
or tonumber(row.tamedAt) ~= nil
|
|
or tonumber(row.tameBeastId) ~= nil
|
|
or (type(row.tameZone) == "string" and row.tameZone ~= "")
|
|
if not hasTameRecord and hasPendingTame then
|
|
local sourceText = tostring(source or "")
|
|
if sourceText == "unit-pet-acquire" or sourceText == "refresh-current-pet" then
|
|
row.tameRecorded = true
|
|
row.tamedAt = row.tamedAt or context.timestamp
|
|
row.tameHunterLevel = row.tameHunterLevel or context.hunterLevel
|
|
row.tameZone = row.tameZone or context.zone
|
|
row.tameSubZone = row.tameSubZone or context.subZone
|
|
row.tameX = row.tameX or context.x
|
|
row.tameY = row.tameY or context.y
|
|
if tonumber(row.tameBeastId) == nil then
|
|
local resolvedTameBeastId = MTH_PETS_ResolveTameBeastIdForRow(row, snapshot)
|
|
if resolvedTameBeastId and resolvedTameBeastId > 0 then
|
|
row.tameBeastId = resolvedTameBeastId
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if type(row.abilities) ~= "table" then
|
|
row.abilities = {}
|
|
end
|
|
MTH_PETS_LogConsistency("ApplySnapshotToRow(after) source=" .. tostring(source) .. " row=" .. MTH_PETS_FormatRowConsistency(row))
|
|
end
|
|
|
|
local function MTH_PETS_UpsertActivePetFromSnapshot(pets, snapshot, source, options)
|
|
if type(pets) ~= "table" or type(snapshot) ~= "table" then
|
|
return nil, false
|
|
end
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
if not petStore then
|
|
return nil, false
|
|
end
|
|
local currentPet = pets.currentPet
|
|
local previousPetId = currentPet and currentPet.id or nil
|
|
local petId = MTH_PETS_SelectPetIdBySnapshot(petStore, snapshot, previousPetId)
|
|
local created = false
|
|
if not petId then
|
|
petId = MTH_PETS_CreatePetId(petStore)
|
|
created = true
|
|
end
|
|
MTH_PETS_LogConsistency("UpsertActivePet select source=" .. tostring(source)
|
|
.. " previousCurrentId=" .. tostring(previousPetId)
|
|
.. " selectedId=" .. tostring(petId)
|
|
.. " created=" .. tostring(created)
|
|
.. " snapshotName='" .. tostring(snapshot.name or "") .. "'")
|
|
|
|
local row = petStore.activeById[petId]
|
|
local wasKnownRow = type(row) == "table"
|
|
if type(row) ~= "table" then
|
|
row = {
|
|
id = petId,
|
|
createdAt = time(),
|
|
firstSeen = time(),
|
|
abilities = {},
|
|
events = {},
|
|
status = "active",
|
|
}
|
|
petStore.activeById[petId] = row
|
|
created = true
|
|
end
|
|
|
|
if row.signature and row.signature ~= snapshot.signature then
|
|
MTH_PETS_RemoveSignatureIndex(petStore, row.signature, petId)
|
|
end
|
|
if row.stableSlot and petStore.stableSlotIndex[row.stableSlot] == petId then
|
|
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
|
|
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
|
|
end
|
|
if snapshot.signature and snapshot.signature ~= "" then
|
|
MTH_PETS_AddSignatureIndex(petStore, snapshot.signature, petId)
|
|
end
|
|
|
|
if created then
|
|
row.origin = source or "unknown"
|
|
row.originContext = context
|
|
local hasPendingTame = type(MTH_PETS_LastTameAttempt) == "table"
|
|
local shouldRecordTame = hasPendingTame and (source == "unit-pet-acquire" or source == "refresh-current-pet")
|
|
if shouldRecordTame then
|
|
row.tamedAt = context.timestamp
|
|
row.tameHunterLevel = context.hunterLevel
|
|
row.tameZone = context.zone
|
|
row.tameSubZone = context.subZone
|
|
row.tameX = context.x
|
|
row.tameY = context.y
|
|
row.tameBeastId = snapshot.beastId
|
|
row.tameRecorded = true
|
|
end
|
|
if type(options) == "table" and options.stableSlot then
|
|
row.origin = "stable-slot"
|
|
end
|
|
end
|
|
|
|
MTH_PETS_SetCurrentPetId(pets, petId)
|
|
pets.updatedAt = time()
|
|
MTH_PETS_LogConsistency("UpsertActivePet finalized activeCurrentId=" .. tostring(petStore.activeCurrentId)
|
|
.. " row=" .. MTH_PETS_FormatRowConsistency(row))
|
|
|
|
return petId, created
|
|
end
|
|
|
|
local function MTH_PETS_RecordStableSlot(pets, slot, raw1, raw2, raw3, raw4, raw5, raw6, raw7, raw8, scanContext, scanStableMasterName)
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
local slotNumber = tonumber(slot)
|
|
if not slotNumber or slotNumber <= 0 then
|
|
return
|
|
end
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
if not petStore then
|
|
return
|
|
end
|
|
|
|
if raw2 == nil or raw2 == "" then
|
|
petStore.stableSlotIndex[slotNumber] = nil
|
|
MTH_PETS_LogConsistency("RecordStableSlot cleared slot=" .. tostring(slotNumber) .. " because name is empty")
|
|
return
|
|
end
|
|
MTH_PETS_LogConsistency("RecordStableSlot slot=" .. tostring(slotNumber)
|
|
.. " name='" .. tostring(raw2 or "") .. "'"
|
|
.. " family='" .. tostring(raw4 or "") .. "'"
|
|
.. " level='" .. tostring(raw3 or "") .. "'")
|
|
|
|
local snapshot = {
|
|
name = raw2,
|
|
family = raw4,
|
|
level = tonumber(raw3) or 0,
|
|
happiness = nil,
|
|
loyalty = nil,
|
|
loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(raw5),
|
|
xp = nil,
|
|
xpMax = nil,
|
|
xpPercent = nil,
|
|
guid = nil,
|
|
beastId = nil,
|
|
signature = MTH_PETS_MakeSignature(raw2, raw4, raw3),
|
|
}
|
|
|
|
local existingId = petStore.stableSlotIndex[slotNumber]
|
|
local petId = nil
|
|
if existingId and petStore.activeById[existingId] then
|
|
local existingRow = petStore.activeById[existingId]
|
|
local existingSignature = existingRow and existingRow.signature or nil
|
|
local snapshotSignature = snapshot and snapshot.signature or nil
|
|
local existingGuid = existingRow and existingRow.guid or nil
|
|
local snapshotGuid = snapshot and snapshot.guid or nil
|
|
local signatureMatches = (existingSignature and snapshotSignature and existingSignature == snapshotSignature) and true or false
|
|
local guidMatches = (existingGuid and snapshotGuid and existingGuid ~= "" and existingGuid == snapshotGuid) and true or false
|
|
|
|
if signatureMatches or guidMatches then
|
|
petId = existingId
|
|
else
|
|
MTH_PETS_LogConsistency("RecordStableSlot remapping stale slot=" .. tostring(slotNumber)
|
|
.. " existingId=" .. tostring(existingId)
|
|
.. " existingSignature='" .. tostring(existingSignature or "") .. "'"
|
|
.. " snapshotSignature='" .. tostring(snapshotSignature or "") .. "'")
|
|
petStore.stableSlotIndex[slotNumber] = nil
|
|
end
|
|
end
|
|
|
|
if not petId then
|
|
petId = MTH_PETS_SelectPetIdBySnapshot(petStore, snapshot, nil)
|
|
end
|
|
|
|
if not petId then
|
|
petId = MTH_PETS_CreatePetId(petStore)
|
|
end
|
|
|
|
local row = petStore.activeById[petId]
|
|
local wasKnownRow = type(row) == "table"
|
|
if type(row) ~= "table" then
|
|
row = {
|
|
id = petId,
|
|
createdAt = time(),
|
|
firstSeen = time(),
|
|
abilities = {},
|
|
events = {},
|
|
status = "active",
|
|
}
|
|
petStore.activeById[petId] = row
|
|
end
|
|
|
|
if row.signature and row.signature ~= snapshot.signature then
|
|
MTH_PETS_RemoveSignatureIndex(petStore, row.signature, petId)
|
|
end
|
|
|
|
local previousStableSlot = tonumber(row.stableSlot)
|
|
local wasPreviouslyStabled = previousStableSlot ~= nil and previousStableSlot > 0
|
|
local lastSourceLower = MTH_PETS_SafeLower(row.lastSource)
|
|
local didEnterStableFromActive = wasKnownRow and not wasPreviouslyStabled and lastSourceLower ~= "stable-scan"
|
|
|
|
local stableContext = type(scanContext) == "table" and scanContext or MTH_PETS_CaptureContext()
|
|
local previousStableInfo = (type(row.stableInfo) == "table") and row.stableInfo or nil
|
|
local stableMasterName = previousStableInfo and tostring(previousStableInfo.stableMasterName or "") or ""
|
|
local stableZone = previousStableInfo and previousStableInfo.stableZone or nil
|
|
local stableSubZone = previousStableInfo and previousStableInfo.stableSubZone or nil
|
|
if didEnterStableFromActive then
|
|
stableMasterName = MTH_PETS_NormalizeText(scanStableMasterName)
|
|
if stableMasterName == "" then
|
|
stableMasterName = MTH_PETS_GetStableMasterName()
|
|
end
|
|
if stableMasterName == "" then
|
|
stableMasterName = ""
|
|
end
|
|
stableZone = stableContext and stableContext.zone or nil
|
|
stableSubZone = stableContext and stableContext.subZone or nil
|
|
end
|
|
if stableSubZone == "" then stableSubZone = nil end
|
|
if stableZone == "" then stableZone = nil end
|
|
MTH_PETS_ApplySnapshotToRow(row, snapshot, "stable-scan", stableContext)
|
|
if tonumber(row.stableSlot) ~= slotNumber then
|
|
if didEnterStableFromActive then
|
|
row.stabledAt = time()
|
|
else
|
|
row.stableFirstSeenAt = row.stableFirstSeenAt or time()
|
|
end
|
|
end
|
|
row.icon = raw1
|
|
row.stableSlot = slotNumber
|
|
row.stableRaw = { raw1, raw2, raw3, raw4, raw5, raw6, raw7, raw8 }
|
|
row.stableInfo = {
|
|
icon = raw1,
|
|
name = raw2,
|
|
level = raw3,
|
|
family = raw4,
|
|
loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(raw5),
|
|
stabledAt = row.stabledAt,
|
|
stableFirstSeenAt = row.stableFirstSeenAt,
|
|
stableZone = stableZone,
|
|
stableSubZone = stableSubZone,
|
|
stableMasterName = stableMasterName,
|
|
r6 = raw6,
|
|
r7 = raw7,
|
|
r8 = raw8,
|
|
updatedAt = (didEnterStableFromActive and time()) or (previousStableInfo and previousStableInfo.updatedAt) or time(),
|
|
}
|
|
|
|
if snapshot.signature and snapshot.signature ~= "" then
|
|
MTH_PETS_AddSignatureIndex(petStore, snapshot.signature, petId)
|
|
end
|
|
petStore.stableSlotIndex[slotNumber] = petId
|
|
pets.updatedAt = time()
|
|
end
|
|
|
|
function MTH_PETS_RecordCurrentPetLearnedAbility(abilityName, rankNumber, source)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return false
|
|
end
|
|
local cp = pets.currentPet
|
|
if type(cp) ~= "table" or cp.exists ~= true or not cp.id then
|
|
return false
|
|
end
|
|
local row = MTH_PETS_GetPetById(pets, cp.id)
|
|
if type(row) ~= "table" then
|
|
return false
|
|
end
|
|
if type(row.abilities) ~= "table" then
|
|
row.abilities = {}
|
|
end
|
|
local nameText = MTH_PETS_NormalizeText(abilityName)
|
|
if nameText == "" then
|
|
return false
|
|
end
|
|
local token = MTH_PETS_SafeLower(nameText)
|
|
local numericRank = tonumber(rankNumber)
|
|
if numericRank and numericRank > 0 then
|
|
token = token .. "#" .. tostring(numericRank)
|
|
end
|
|
row.abilities[token] = {
|
|
name = nameText,
|
|
rank = numericRank,
|
|
source = source or "unknown",
|
|
recordedAt = time(),
|
|
}
|
|
row.lastUpdated = time()
|
|
pets.updatedAt = time()
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_RecordCurrentPetSpellbookSnapshot(spells, source)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return 0, false
|
|
end
|
|
local cp = pets.currentPet
|
|
if type(cp) ~= "table" or cp.exists ~= true or not cp.id then
|
|
return 0, false
|
|
end
|
|
local row = MTH_PETS_GetPetById(pets, cp.id)
|
|
if type(row) ~= "table" then
|
|
return 0, false
|
|
end
|
|
if type(spells) ~= "table" then
|
|
spells = {}
|
|
end
|
|
|
|
local stored = {}
|
|
local count = 0
|
|
for i = 1, table.getn(spells) do
|
|
local spell = spells[i]
|
|
if type(spell) == "table" and tostring(spell.name or "") ~= "" then
|
|
count = count + 1
|
|
stored[count] = {
|
|
name = tostring(spell.name or ""),
|
|
rank = spell.rank,
|
|
icon = spell.icon,
|
|
isPassive = spell.isPassive and true or false,
|
|
slot = tonumber(spell.slot) or count,
|
|
token = spell.token,
|
|
}
|
|
end
|
|
end
|
|
|
|
local existingSpellbook = row.petSpellbook
|
|
local existingCount = 0
|
|
if type(existingSpellbook) == "table" then
|
|
existingCount = tonumber(existingSpellbook.count) or 0
|
|
end
|
|
if count <= 0 and existingCount > 0 then
|
|
row.lastPetSpellbookSkipAt = time()
|
|
row.lastPetSpellbookSkipSource = source or "unknown"
|
|
row.lastPetSpellbookSkipReason = "empty-scan-preserved-existing"
|
|
row.lastUpdated = time()
|
|
pets.updatedAt = time()
|
|
return existingCount, false
|
|
end
|
|
|
|
row.petSpellbook = {
|
|
updatedAt = time(),
|
|
source = source or "unknown",
|
|
count = count,
|
|
spells = stored,
|
|
}
|
|
row.lastUpdated = time()
|
|
pets.updatedAt = time()
|
|
return count, true
|
|
end
|
|
|
|
local function MTH_PETS_MoveToHistory(pets, petId, source, context, reason)
|
|
if type(pets) ~= "table" or not petId then
|
|
return false
|
|
end
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
if type(petStore) ~= "table" then
|
|
return false
|
|
end
|
|
local resolvedPetId = petId
|
|
local row = petStore and petStore.activeById and petStore.activeById[resolvedPetId]
|
|
if type(row) ~= "table" then
|
|
local normalizedPetId = MTH_PETS_NormalizePetId(petId)
|
|
if normalizedPetId ~= nil then
|
|
resolvedPetId = normalizedPetId
|
|
row = petStore and petStore.activeById and petStore.activeById[resolvedPetId]
|
|
end
|
|
end
|
|
if type(row) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
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
|
|
end
|
|
if row.stableSlot and petStore.stableSlotIndex[row.stableSlot] == resolvedPetId then
|
|
petStore.stableSlotIndex[row.stableSlot] = nil
|
|
end
|
|
|
|
row.status = "abandoned"
|
|
row.abandonedAt = (context and context.timestamp) or time()
|
|
row.abandonContext = context or MTH_PETS_CaptureContext()
|
|
row.abandonSource = source or "unknown"
|
|
row.abandonReason = reason or "abandon"
|
|
row.lastUpdated = time()
|
|
|
|
petStore.historyById[resolvedPetId] = row
|
|
petStore.activeById[resolvedPetId] = nil
|
|
if tostring(petStore.activeCurrentId or "") == tostring(resolvedPetId)
|
|
or tostring(pets.currentPetId or "") == tostring(resolvedPetId) then
|
|
MTH_PETS_SetCurrentPetId(pets, nil)
|
|
end
|
|
pets.updatedAt = time()
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_RecordPetAbandon(source, explicitName)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return false
|
|
end
|
|
local cp = pets.currentPet
|
|
local petId = cp and cp.id or nil
|
|
if not petId then
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
if type(petStore) ~= "table" then
|
|
return false
|
|
end
|
|
local petName = MTH_PETS_NormalizeText(explicitName or (cp and cp.name) or "")
|
|
local petFamily = cp and cp.family or nil
|
|
local petLevel = cp and cp.level or nil
|
|
local signature = MTH_PETS_MakeSignature(petName, petFamily, petLevel)
|
|
local list = petStore.signatureIndex[signature]
|
|
if type(list) == "table" and table.getn(list) == 1 then
|
|
petId = list[1]
|
|
end
|
|
end
|
|
|
|
local context = MTH_PETS_CaptureContext()
|
|
local ok = MTH_PETS_MoveToHistory(pets, petId, source or "abandon", context, "pet-abandon")
|
|
|
|
if type(cp) == "table" then
|
|
cp.exists = false
|
|
cp.id = nil
|
|
cp.guid = nil
|
|
cp.signature = nil
|
|
cp.beastId = nil
|
|
cp.name = nil
|
|
cp.family = nil
|
|
cp.level = nil
|
|
cp.happiness = nil
|
|
cp.loyalty = nil
|
|
cp.loyaltyLevel = nil
|
|
cp.xp = nil
|
|
cp.xpMax = nil
|
|
cp.xpPercent = nil
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = source or "abandon"
|
|
end
|
|
MTH_PETS_SetCurrentPetId(pets, nil)
|
|
MTH_PETS_SetCurrentPetSuppressed(pets, true, "abandon")
|
|
MTH_PETS_LastTameAttempt = nil
|
|
|
|
return ok
|
|
end
|
|
|
|
function MTH_PETS_RecordPetRunaway(source, rawMessage)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local cp = pets.currentPet
|
|
local petId = cp and cp.id or nil
|
|
local petName = cp and cp.name or nil
|
|
local context = MTH_PETS_CaptureContext()
|
|
context.systemMessage = MTH_PETS_NormalizeSystemMessage(rawMessage)
|
|
|
|
local ok = MTH_PETS_MoveToHistory(pets, petId, source or "runaway", context, "pet-runaway")
|
|
if not ok then
|
|
return false
|
|
end
|
|
|
|
if type(cp) == "table" then
|
|
cp.exists = false
|
|
cp.id = nil
|
|
cp.guid = nil
|
|
cp.signature = nil
|
|
cp.beastId = nil
|
|
cp.name = nil
|
|
cp.family = nil
|
|
cp.level = nil
|
|
cp.happiness = nil
|
|
cp.loyalty = nil
|
|
cp.loyaltyLevel = nil
|
|
cp.xp = nil
|
|
cp.xpMax = nil
|
|
cp.xpPercent = nil
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = source or "runaway"
|
|
end
|
|
MTH_PETS_SetCurrentPetId(pets, nil)
|
|
MTH_PETS_SetCurrentPetSuppressed(pets, true, "runaway")
|
|
MTH_PETS_LastTameAttempt = nil
|
|
|
|
MTH_ST_LastUnitPetHadPet = false
|
|
pets.updatedAt = time()
|
|
|
|
if MTH and MTH.Print then
|
|
local displayName = MTH_PETS_NormalizeText(petName)
|
|
if displayName == "" then displayName = "Unknown" end
|
|
if MTH:IsMessageEnabled("petRanAway", true) then
|
|
MTH:Print("YOUR PET " .. tostring(displayName) .. " HAS RUNAWAY FOREVER :-( Try to feed it better next time.")
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_RecordPetRename(oldName, newName, source)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return false
|
|
end
|
|
local cp = pets.currentPet
|
|
local petId = cp and cp.id or nil
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
local petRow = MTH_PETS_GetPetById(pets, petId)
|
|
if type(petRow) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local normalizedNewName = MTH_PETS_NormalizeText(newName)
|
|
if normalizedNewName == "" then
|
|
return false
|
|
end
|
|
local oldSignature = petRow.signature
|
|
petRow.previousName = MTH_PETS_NormalizeText(oldName)
|
|
petRow.name = normalizedNewName
|
|
petRow.signature = MTH_PETS_MakeSignature(petRow.name, petRow.family, petRow.level)
|
|
MTH_PETS_RemoveSignatureIndex(petStore, oldSignature, petId)
|
|
MTH_PETS_AddSignatureIndex(petStore, petRow.signature, petId)
|
|
petRow.lastUpdated = time()
|
|
petRow.lastSource = source or "rename"
|
|
|
|
if type(petRow.events) ~= "table" then
|
|
petRow.events = {}
|
|
end
|
|
table.insert(petRow.events, {
|
|
type = "pet-rename",
|
|
from = MTH_PETS_NormalizeText(oldName),
|
|
to = normalizedNewName,
|
|
source = source or "rename",
|
|
at = time(),
|
|
context = MTH_PETS_CaptureContext(),
|
|
})
|
|
|
|
cp.name = normalizedNewName
|
|
cp.signature = petRow.signature
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = source or "rename"
|
|
pets.updatedAt = time()
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_CorePetRenameHook(newName)
|
|
local oldName = nil
|
|
if type(UnitName) == "function" then
|
|
oldName = UnitName("pet")
|
|
end
|
|
if (not oldName or oldName == "") and MTH_CharSavedVariables and type(MTH_CharSavedVariables.MTH_Pets) == "table" then
|
|
local cp = MTH_CharSavedVariables.MTH_Pets.currentPet
|
|
oldName = cp and cp.name or oldName
|
|
end
|
|
|
|
local newNameNormalized = MTH_PETS_NormalizeText(newName)
|
|
if newNameNormalized ~= "" then
|
|
if type(MTH_PETS_RecordPetRename) == "function" then
|
|
MTH_PETS_RecordPetRename(oldName, newNameNormalized, "core:PetRename")
|
|
end
|
|
|
|
if type(FOM_FoodQuality) == "table" and type(FOM_RealmPlayer) == "string" and FOM_RealmPlayer ~= "" then
|
|
if type(FOM_FoodQuality[FOM_RealmPlayer]) == "table" then
|
|
local realmMap = FOM_FoodQuality[FOM_RealmPlayer]
|
|
local oldKey = MTH_PETS_NormalizeText(oldName)
|
|
if oldKey ~= "" and oldKey ~= newNameNormalized and realmMap[oldKey] ~= nil then
|
|
realmMap[newNameNormalized] = realmMap[oldKey]
|
|
realmMap[oldKey] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if type(MTH_PETS_CoreOriginal_PetRename) == "function" and MTH_PETS_CoreOriginal_PetRename ~= MTH_PETS_CorePetRenameHook then
|
|
return MTH_PETS_CoreOriginal_PetRename(newName)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function MTH_PETS_InstallCoreRenameHook()
|
|
if type(PetRename) ~= "function" then
|
|
return false
|
|
end
|
|
if PetRename == MTH_PETS_CorePetRenameHook then
|
|
if MTH and MTH.CaptureHookBoundary then
|
|
MTH:CaptureHookBoundary(MTH_PETS_CORE_HOOK_BOUNDARY_KEY, {
|
|
{ globalName = "PetRename", originalName = "MTH_PETS_CoreOriginal_PetRename" },
|
|
})
|
|
end
|
|
return true
|
|
end
|
|
|
|
if type(MTH_PETS_CoreOriginal_PetRename) ~= "function" or MTH_PETS_CoreOriginal_PetRename == MTH_PETS_CorePetRenameHook then
|
|
MTH_PETS_CoreOriginal_PetRename = PetRename
|
|
end
|
|
|
|
if MTH_PETS_CoreOriginal_PetRename == MTH_PETS_CorePetRenameHook then
|
|
return false
|
|
end
|
|
|
|
PetRename = MTH_PETS_CorePetRenameHook
|
|
if MTH and MTH.CaptureHookBoundary then
|
|
MTH:CaptureHookBoundary(MTH_PETS_CORE_HOOK_BOUNDARY_KEY, {
|
|
{ globalName = "PetRename", originalName = "MTH_PETS_CoreOriginal_PetRename" },
|
|
})
|
|
end
|
|
return true
|
|
end
|
|
|
|
function MTH_PETS_GetRootStore()
|
|
if not MTH_CharSavedVariables then
|
|
MTH_CharSavedVariables = {}
|
|
end
|
|
|
|
if type(MTH_CharSavedVariables.MTH_Pets) ~= "table" then
|
|
MTH_CharSavedVariables.MTH_Pets = {
|
|
petTraining = {},
|
|
trainScan = {},
|
|
stableScan = {},
|
|
petStore = {},
|
|
}
|
|
end
|
|
|
|
local pets = MTH_CharSavedVariables.MTH_Pets
|
|
if type(pets.petTraining) ~= "table" then
|
|
pets.petTraining = {}
|
|
end
|
|
if type(pets.trainScan) ~= "table" then
|
|
pets.trainScan = pets.petTraining
|
|
end
|
|
if type(pets.stableScan) ~= "table" then
|
|
pets.stableScan = {}
|
|
end
|
|
if type(pets.petStore) ~= "table" then
|
|
pets.petStore = {}
|
|
end
|
|
|
|
MTH_PETS_EnsureSchema(pets)
|
|
MTH_PETS_EnsurePetTrainingCompatibility(pets)
|
|
|
|
-- Migration: remove root-level duplicates that bloated SavedVariables
|
|
if pets._migrationCleanup ~= 2 then
|
|
MTH_PETS_RunSavedVarCleanup(pets)
|
|
pets._migrationCleanup = 2
|
|
end
|
|
|
|
return pets
|
|
end
|
|
|
|
function MTH_PETS_RunSavedVarCleanup(pets)
|
|
-- 1. Remove root-level duplicates (data lives inside MTH_Pets only)
|
|
if MTH_CharSavedVariables then
|
|
MTH_CharSavedVariables.petTraining = nil
|
|
MTH_CharSavedVariables.trainScan = nil
|
|
MTH_CharSavedVariables.stableScan = nil
|
|
MTH_CharSavedVariables.petStore = nil
|
|
end
|
|
|
|
-- 2. Purge pet-updated events (unbounded bloat)
|
|
local purged = 0
|
|
local stores = { pets.petStore and pets.petStore.activeById, pets.petStore and pets.petStore.historyById }
|
|
for _, index in ipairs(stores) do
|
|
if type(index) == "table" then
|
|
for petId, row in pairs(index) do
|
|
if type(row) == "table" and type(row.events) == "table" then
|
|
local kept = {}
|
|
for _, evt in ipairs(row.events) do
|
|
if type(evt) == "table" and evt.type ~= "pet-updated" then
|
|
table.insert(kept, evt)
|
|
else
|
|
purged = purged + 1
|
|
end
|
|
end
|
|
row.events = kept
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 3. Remove stale _betaFreshStoreApplied flag
|
|
pets._betaFreshStoreApplied = nil
|
|
|
|
if purged > 0 and MTH and MTH.Print then
|
|
MTH:Print("Cleaned up " .. tostring(purged) .. " stale pet-update records from SavedVariables.", "debug")
|
|
end
|
|
end
|
|
|
|
function MTH_PETS_RefreshCurrentPet()
|
|
local pets = type(MTH_PETS_GetRootStore) == "function" and MTH_PETS_GetRootStore() or nil
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
|
|
local cp = pets.currentPet
|
|
local previousCurrentId = cp and cp.id or nil
|
|
local hadPetBeforeRefresh = MTH_ST_LastUnitPetHadPet and true or false
|
|
local now = time()
|
|
local snapshot = MTH_PETS_MakeSnapshotFromLivePet()
|
|
if pets.currentPetSuppressed == true and pets.currentPetSuppressedAwaitNoLive == true then
|
|
if type(snapshot) == "table" then
|
|
snapshot = nil
|
|
else
|
|
pets.currentPetSuppressedAwaitNoLive = false
|
|
end
|
|
end
|
|
local hadPendingTame = type(MTH_PETS_LastTameAttempt) == "table"
|
|
if hadPendingTame then
|
|
MTH_PETS_LogTame("RefreshCurrentPet with pending tame attempt previousCurrentId=" .. tostring(previousCurrentId)
|
|
.. " liveSnapshot=" .. tostring(type(snapshot) == "table"))
|
|
end
|
|
MTH_PETS_LogConsistency("RefreshCurrentPet start current=" .. MTH_PETS_FormatCurrentConsistency(cp)
|
|
.. " previousCurrentId=" .. tostring(previousCurrentId)
|
|
.. " hasLiveSnapshot=" .. tostring(type(snapshot) == "table"))
|
|
if not snapshot then
|
|
cp.exists = false
|
|
local keepCurrentId, keepCurrentRow = MTH_PETS_ResolveNoLiveCurrentRow(pets, previousCurrentId)
|
|
if keepCurrentId ~= nil and type(keepCurrentRow) == "table" then
|
|
MTH_PETS_ApplyCurrentPetFromActiveRow(cp, keepCurrentId, keepCurrentRow)
|
|
else
|
|
cp.id = nil
|
|
cp.guid = nil
|
|
cp.signature = nil
|
|
cp.beastId = nil
|
|
cp.name = nil
|
|
cp.family = nil
|
|
cp.level = nil
|
|
cp.happiness = nil
|
|
cp.loyalty = nil
|
|
cp.loyaltyLevel = nil
|
|
cp.xp = nil
|
|
cp.xpMax = nil
|
|
cp.xpPercent = nil
|
|
end
|
|
cp.lastUpdated = now
|
|
cp.lastSource = "no-pet"
|
|
local petStore = MTH_PETS_GetStoreTables(pets)
|
|
MTH_PETS_SetCurrentPetId(pets, keepCurrentId)
|
|
MTH_PETS_LogConsistency("RefreshCurrentPet no-live result current=" .. MTH_PETS_FormatCurrentConsistency(cp)
|
|
.. " activeCurrentId=" .. tostring(petStore and petStore.activeCurrentId or nil))
|
|
MTH_ST_LastUnitPetHadPet = false
|
|
pets.updatedAt = now
|
|
return
|
|
end
|
|
|
|
if snapshot.beastId == nil and hadPendingTame then
|
|
snapshot.beastId = MTH_PETS_GetPendingTameBeastId(snapshot)
|
|
MTH_PETS_LogTame("RefreshCurrentPet resolved beastId from pending tame => " .. tostring(snapshot.beastId or "nil"))
|
|
end
|
|
|
|
local petId, created = MTH_PETS_UpsertActivePetFromSnapshot(pets, snapshot, "refresh-current-pet", nil)
|
|
MTH_PETS_SetCurrentPetSuppressed(pets, false)
|
|
cp.exists = true
|
|
cp.id = petId
|
|
cp.guid = snapshot.guid
|
|
cp.signature = snapshot.signature
|
|
cp.beastId = snapshot.beastId
|
|
cp.name = snapshot.name
|
|
cp.family = snapshot.family
|
|
cp.level = snapshot.level
|
|
cp.happiness = snapshot.happiness
|
|
cp.loyalty = snapshot.loyalty
|
|
cp.loyaltyLevel = snapshot.loyaltyLevel
|
|
cp.xp = snapshot.xp
|
|
cp.xpMax = snapshot.xpMax
|
|
cp.xpPercent = snapshot.xpPercent
|
|
cp.lastUpdated = now
|
|
cp.lastSeen = now
|
|
if created then
|
|
cp.acquiredAt = now
|
|
cp.lastSource = "acquired"
|
|
else
|
|
cp.lastSource = "refresh"
|
|
end
|
|
if cp.id and cp.id ~= previousCurrentId then
|
|
MTH_PETS_RequestPetSpellScan("refresh-current-change")
|
|
end
|
|
|
|
local row = MTH_PETS_GetPetById(pets, petId)
|
|
if type(row) == "table" then
|
|
if type(row.events) ~= "table" then
|
|
row.events = {}
|
|
end
|
|
|
|
local eventType = "pet-acquired"
|
|
if not (created and (not hadPetBeforeRefresh)) then
|
|
if cp.id ~= nil and previousCurrentId ~= nil and cp.id ~= previousCurrentId then
|
|
eventType = "pet-swapped"
|
|
else
|
|
eventType = nil -- routine update, don't record
|
|
end
|
|
end
|
|
|
|
local eventContext = nil
|
|
if eventType then
|
|
eventContext = MTH_PETS_CaptureContext()
|
|
table.insert(row.events, {
|
|
type = eventType,
|
|
source = "refresh-current-pet",
|
|
at = now,
|
|
context = eventContext,
|
|
})
|
|
end
|
|
|
|
if eventType == "pet-acquired" then
|
|
local hasTameRecord = (row.tameRecorded == true)
|
|
or (tonumber(row.tamedAt) and tonumber(row.tamedAt) > 0)
|
|
or (type(row.tameZone) == "string" and row.tameZone ~= "")
|
|
if not hasTameRecord and type(eventContext) == "table" then
|
|
row.tameRecorded = true
|
|
row.tamedAt = row.tamedAt or eventContext.timestamp or now
|
|
row.tameHunterLevel = row.tameHunterLevel or eventContext.hunterLevel
|
|
row.tameZone = row.tameZone or eventContext.zone
|
|
row.tameSubZone = row.tameSubZone or eventContext.subZone
|
|
row.tameX = row.tameX or eventContext.x
|
|
row.tameY = row.tameY or eventContext.y
|
|
if tonumber(row.tameBeastId) == nil then
|
|
local resolvedTameBeastId = MTH_PETS_ResolveTameBeastIdForRow(row, snapshot)
|
|
if resolvedTameBeastId and resolvedTameBeastId > 0 then
|
|
row.tameBeastId = resolvedTameBeastId
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if hadPendingTame and cp.id and cp.id ~= previousCurrentId then
|
|
MTH_PETS_LogTame("RefreshCurrentPet acquire/change completed; clearing pending tame attempt")
|
|
MTH_PETS_LastTameAttempt = nil
|
|
end
|
|
MTH_PETS_LogConsistency("RefreshCurrentPet live result current=" .. MTH_PETS_FormatCurrentConsistency(cp))
|
|
MTH_ST_LastUnitPetHadPet = true
|
|
pets.updatedAt = now
|
|
end
|
|
|
|
function MTH_GetPetsStore()
|
|
return MTH_PETS_GetRootStore()
|
|
end
|
|
|
|
function MTH_GetCurrentPetSnapshot()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
MTH_PETS_RefreshCurrentPet()
|
|
return pets and pets.currentPet or nil
|
|
end
|
|
|
|
local function MTH_PETS_ApplyLivePetVitalsFast(source)
|
|
local pets = type(MTH_PETS_GetRootStore) == "function" and MTH_PETS_GetRootStore() or nil
|
|
if type(pets) ~= "table" then
|
|
return false
|
|
end
|
|
MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
|
|
local cp = pets.currentPet
|
|
if type(cp) ~= "table" or cp.id == nil then
|
|
return false
|
|
end
|
|
if type(UnitExists) ~= "function" or not UnitExists("pet") then
|
|
return false
|
|
end
|
|
|
|
local changed = false
|
|
local function assignIfChanged(target, key, value)
|
|
if type(target) ~= "table" then
|
|
return
|
|
end
|
|
if not MTH_PETS_IsSameLiveStateField(target[key], value) then
|
|
target[key] = value
|
|
changed = true
|
|
end
|
|
end
|
|
|
|
local happiness = nil
|
|
local loyalty = nil
|
|
if type(GetPetHappiness) == "function" then
|
|
local h, _, l = GetPetHappiness()
|
|
happiness = h
|
|
loyalty = l
|
|
end
|
|
|
|
local loyaltyLevel = nil
|
|
local getPetLoyalty = (type(getglobal) == "function" and getglobal("GetPetLoyalty")) or (_G and _G["GetPetLoyalty"])
|
|
if type(getPetLoyalty) == "function" then
|
|
local level = getPetLoyalty()
|
|
loyaltyLevel = tonumber(level) or nil
|
|
end
|
|
|
|
local xp = nil
|
|
local xpMax = nil
|
|
local xpPercent = nil
|
|
local getPetExperience = (type(getglobal) == "function" and getglobal("GetPetExperience")) or (_G and _G["GetPetExperience"])
|
|
if type(getPetExperience) == "function" then
|
|
local x, xMax = getPetExperience()
|
|
xp = tonumber(x)
|
|
xpMax = tonumber(xMax)
|
|
if xp and xpMax and xpMax > 0 then
|
|
xpPercent = math.floor((xp / xpMax) * 1000 + 0.5) / 10
|
|
end
|
|
end
|
|
|
|
assignIfChanged(cp, "happiness", happiness)
|
|
assignIfChanged(cp, "loyalty", loyalty)
|
|
if loyaltyLevel ~= nil then
|
|
assignIfChanged(cp, "loyaltyLevel", loyaltyLevel)
|
|
end
|
|
assignIfChanged(cp, "xp", xp)
|
|
assignIfChanged(cp, "xpMax", xpMax)
|
|
assignIfChanged(cp, "xpPercent", xpPercent)
|
|
|
|
local activeRow = nil
|
|
if type(pets.petStore) == "table" and type(pets.petStore.activeById) == "table" then
|
|
activeRow = pets.petStore.activeById[cp.id] or pets.petStore.activeById[tostring(cp.id)]
|
|
end
|
|
if type(activeRow) == "table" then
|
|
assignIfChanged(activeRow, "happiness", happiness)
|
|
assignIfChanged(activeRow, "loyalty", loyalty)
|
|
if loyaltyLevel ~= nil then
|
|
assignIfChanged(activeRow, "loyaltyLevel", loyaltyLevel)
|
|
end
|
|
assignIfChanged(activeRow, "xp", xp)
|
|
assignIfChanged(activeRow, "xpMax", xpMax)
|
|
assignIfChanged(activeRow, "xpPercent", xpPercent)
|
|
if changed then
|
|
activeRow.lastUpdated = time()
|
|
end
|
|
end
|
|
|
|
if changed then
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = source or "live-vitals"
|
|
pets.updatedAt = cp.lastUpdated
|
|
end
|
|
|
|
return changed
|
|
end
|
|
|
|
function MTH_GetCurrentPetInfo(skipRefresh)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if not skipRefresh then
|
|
MTH_PETS_RefreshCurrentPet()
|
|
end
|
|
local cp = pets and pets.currentPet or nil
|
|
|
|
local info = {
|
|
exists = false,
|
|
liveExists = false,
|
|
dead = false,
|
|
suppressed = false,
|
|
suppressedReason = "",
|
|
id = nil,
|
|
name = nil,
|
|
family = nil,
|
|
level = nil,
|
|
happiness = nil,
|
|
loyalty = nil,
|
|
loyaltyLevel = nil,
|
|
loyaltyText = nil,
|
|
loyaltyDisplay = nil,
|
|
xp = nil,
|
|
xpMax = nil,
|
|
xpPercent = nil,
|
|
lastSource = nil,
|
|
lastUpdated = nil,
|
|
acquiredAt = nil,
|
|
tamedAt = nil,
|
|
stabledAt = nil,
|
|
stableFirstSeenAt = nil,
|
|
lastUnstabledAt = nil,
|
|
withMeSinceAt = nil,
|
|
}
|
|
|
|
if type(cp) == "table" then
|
|
info.id = cp.id or pets.currentPetId
|
|
info.name = cp.name
|
|
info.family = cp.family
|
|
info.level = cp.level
|
|
info.happiness = cp.happiness
|
|
info.loyalty = cp.loyalty
|
|
info.loyaltyLevel = tonumber(cp.loyaltyLevel) or nil
|
|
info.lastSource = cp.lastSource
|
|
info.lastUpdated = cp.lastUpdated
|
|
info.acquiredAt = tonumber(cp.acquiredAt) or nil
|
|
info.exists = info.id ~= nil and tostring(info.id) ~= ""
|
|
end
|
|
|
|
local activeRow = nil
|
|
if type(pets) == "table" and type(pets.petStore) == "table" and type(pets.petStore.activeById) == "table" and info.id ~= nil then
|
|
activeRow = pets.petStore.activeById[info.id] or pets.petStore.activeById[tostring(info.id)]
|
|
end
|
|
if type(activeRow) == "table" then
|
|
info.tamedAt = tonumber(activeRow.tamedAt) or nil
|
|
info.stabledAt = tonumber(activeRow.stabledAt) or nil
|
|
info.stableFirstSeenAt = tonumber(activeRow.stableFirstSeenAt) or nil
|
|
info.lastUnstabledAt = tonumber(activeRow.lastUnstabledAt) or nil
|
|
if info.acquiredAt == nil then
|
|
info.acquiredAt = tonumber(activeRow.createdAt) or tonumber(activeRow.lastSeen) or tonumber(activeRow.lastUpdated) or nil
|
|
end
|
|
if info.loyaltyLevel == nil then
|
|
info.loyaltyLevel = tonumber(activeRow.loyaltyLevel) or nil
|
|
end
|
|
end
|
|
|
|
info.withMeSinceAt = info.lastUnstabledAt or info.stabledAt or info.stableFirstSeenAt or info.tamedAt or nil
|
|
|
|
info.suppressed = (pets and pets.currentPetSuppressed) and true or false
|
|
info.suppressedReason = tostring((pets and pets.currentPetSuppressedReason) or "")
|
|
|
|
local hasLivePet = (type(UnitExists) == "function") and UnitExists("pet") and true or false
|
|
info.liveExists = hasLivePet
|
|
|
|
if hasLivePet then
|
|
if type(UnitName) == "function" then
|
|
local liveName = UnitName("pet")
|
|
if liveName and tostring(liveName) ~= "" then
|
|
info.name = liveName
|
|
end
|
|
end
|
|
if type(UnitLevel) == "function" then
|
|
local liveLevel = tonumber(UnitLevel("pet"))
|
|
if liveLevel and liveLevel > 0 then
|
|
info.level = liveLevel
|
|
end
|
|
end
|
|
if type(UnitIsDead) == "function" then
|
|
info.dead = UnitIsDead("pet") and true or false
|
|
end
|
|
if type(GetPetHappiness) == "function" then
|
|
local h, _, l = GetPetHappiness()
|
|
if h ~= nil then
|
|
info.happiness = h
|
|
end
|
|
if l ~= nil then
|
|
info.loyalty = l
|
|
end
|
|
end
|
|
local getPetLoyalty = (type(getglobal) == "function" and getglobal("GetPetLoyalty")) or (_G and _G["GetPetLoyalty"])
|
|
if type(getPetLoyalty) == "function" then
|
|
local loyaltyLevel, loyaltyText = getPetLoyalty()
|
|
if loyaltyLevel ~= nil then
|
|
info.loyaltyLevel = tonumber(loyaltyLevel) or info.loyaltyLevel
|
|
end
|
|
if loyaltyText ~= nil and tostring(loyaltyText) ~= "" then
|
|
local loyaltyTextValue = tostring(loyaltyText)
|
|
local parsedFromText = MTH_PETS_ParseLoyaltyLevelFromText(loyaltyTextValue)
|
|
if info.loyaltyLevel == nil and parsedFromText ~= nil then
|
|
info.loyaltyLevel = parsedFromText
|
|
end
|
|
end
|
|
end
|
|
local petFrameHappiness = (_G and _G["PetFrameHappiness"]) or (type(getglobal) == "function" and getglobal("PetFrameHappiness"))
|
|
if type(petFrameHappiness) == "table" and type(petFrameHappiness.tooltipLoyalty) == "string" and petFrameHappiness.tooltipLoyalty ~= "" then
|
|
local tooltipText = tostring(petFrameHappiness.tooltipLoyalty)
|
|
local parsedFromTooltip = MTH_PETS_ParseLoyaltyLevelFromText(tooltipText)
|
|
if info.loyaltyLevel == nil and parsedFromTooltip ~= nil then
|
|
info.loyaltyLevel = parsedFromTooltip
|
|
end
|
|
end
|
|
local petLoyaltyText = (_G and _G["PetLoyaltyText"]) or (type(getglobal) == "function" and getglobal("PetLoyaltyText"))
|
|
if type(petLoyaltyText) == "table" and type(petLoyaltyText.GetText) == "function" then
|
|
local uiLoyaltyText = MTH_PETS_NormalizeText(petLoyaltyText:GetText())
|
|
if uiLoyaltyText ~= "" then
|
|
local parsedFromUi = MTH_PETS_ParseLoyaltyLevelFromText(uiLoyaltyText)
|
|
if info.loyaltyLevel == nil and parsedFromUi ~= nil then
|
|
info.loyaltyLevel = parsedFromUi
|
|
end
|
|
end
|
|
end
|
|
local getPetExperience = (type(getglobal) == "function" and getglobal("GetPetExperience")) or (_G and _G["GetPetExperience"])
|
|
if type(getPetExperience) == "function" then
|
|
local xp, xpMax = getPetExperience()
|
|
xp = tonumber(xp)
|
|
xpMax = tonumber(xpMax)
|
|
if xp then
|
|
info.xp = xp
|
|
end
|
|
if xpMax and xpMax > 0 then
|
|
info.xpMax = xpMax
|
|
if xp then
|
|
info.xpPercent = math.floor((xp / xpMax) * 1000 + 0.5) / 10
|
|
end
|
|
end
|
|
end
|
|
if type(UnitHealth) == "function" then
|
|
info.health = tonumber(UnitHealth("pet")) or nil
|
|
end
|
|
if type(UnitHealthMax) == "function" then
|
|
info.healthMax = tonumber(UnitHealthMax("pet")) or nil
|
|
end
|
|
end
|
|
|
|
if info.loyaltyLevel ~= nil then
|
|
info.loyaltyText = MTH_PETS_GetLoyaltyNameByLevel(info.loyaltyLevel)
|
|
info.loyaltyDisplay = MTH_PETS_FormatLoyaltyDisplay(info.loyaltyLevel)
|
|
else
|
|
info.loyaltyText = nil
|
|
info.loyaltyDisplay = nil
|
|
end
|
|
|
|
if info.liveExists and info.id and type(cp) == "table" then
|
|
local changed = false
|
|
local function assignIfChanged(target, key, value)
|
|
if type(target) ~= "table" then
|
|
return
|
|
end
|
|
if not MTH_PETS_IsSameLiveStateField(target[key], value) then
|
|
target[key] = value
|
|
changed = true
|
|
end
|
|
end
|
|
|
|
assignIfChanged(cp, "happiness", info.happiness)
|
|
assignIfChanged(cp, "loyalty", info.loyalty)
|
|
if info.loyaltyLevel ~= nil then
|
|
assignIfChanged(cp, "loyaltyLevel", info.loyaltyLevel)
|
|
end
|
|
assignIfChanged(cp, "xp", info.xp)
|
|
assignIfChanged(cp, "xpMax", info.xpMax)
|
|
assignIfChanged(cp, "xpPercent", info.xpPercent)
|
|
|
|
if type(activeRow) == "table" then
|
|
assignIfChanged(activeRow, "happiness", info.happiness)
|
|
assignIfChanged(activeRow, "loyalty", info.loyalty)
|
|
if info.loyaltyLevel ~= nil then
|
|
assignIfChanged(activeRow, "loyaltyLevel", info.loyaltyLevel)
|
|
end
|
|
assignIfChanged(activeRow, "xp", info.xp)
|
|
assignIfChanged(activeRow, "xpMax", info.xpMax)
|
|
assignIfChanged(activeRow, "xpPercent", info.xpPercent)
|
|
if changed then
|
|
activeRow.lastUpdated = time()
|
|
end
|
|
end
|
|
|
|
if changed then
|
|
cp.lastUpdated = time()
|
|
pets.updatedAt = cp.lastUpdated
|
|
end
|
|
end
|
|
|
|
return info
|
|
end
|
|
|
|
function MTH_GetPetDatastoreSnapshot()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" or type(pets.petStore) ~= "table" then
|
|
return nil
|
|
end
|
|
return pets.petStore
|
|
end
|
|
|
|
function MTH_GetPetHistorySnapshot()
|
|
local store = MTH_GetPetDatastoreSnapshot()
|
|
if type(store) ~= "table" then
|
|
return nil
|
|
end
|
|
return store.historyById
|
|
end
|
|
|
|
local function MTH_ST_GetStore()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets.stableScan) ~= "table" then
|
|
pets.stableScan = {
|
|
lastScan = 0,
|
|
lastEvent = "",
|
|
slotCount = 0,
|
|
slots = {},
|
|
}
|
|
end
|
|
if type(pets.stableScan.slots) ~= "table" then
|
|
pets.stableScan.slots = {}
|
|
end
|
|
MTH_CharSavedVariables.stableScan = pets.stableScan
|
|
return pets.stableScan
|
|
end
|
|
|
|
function MTH_ST_Scan(reason)
|
|
local startedAt = MTH_ST_ProfileNow()
|
|
MTH_ST_PrintStableDiag("scan begin reason=" .. tostring(reason or ""))
|
|
local slotDiagParts = {}
|
|
if UnitClass then
|
|
local _, classToken = UnitClass("player")
|
|
if classToken ~= "HUNTER" then
|
|
MTH_ST_TraceRecord("MTH_ST_Scan", "scan", "class-gate:" .. tostring(classToken or "unknown"))
|
|
MTH_ST_PrintStableDiag("scan skip reason=class-gate class=" .. tostring(classToken or "unknown"))
|
|
return false
|
|
end
|
|
end
|
|
|
|
if type(GetNumStableSlots) ~= "function" or type(GetStablePetInfo) ~= "function" then
|
|
MTH_ST_Log("scan skipped: stable APIs unavailable")
|
|
MTH_ST_TraceRecord("MTH_ST_Scan", "scan", "api-unavailable")
|
|
MTH_ST_PrintStableDiag("scan skip reason=api-unavailable")
|
|
return false
|
|
end
|
|
|
|
local slotCount = tonumber(GetNumStableSlots()) or 0
|
|
MTH_ST_PrintStableDiag("scan api-ok slots=" .. tostring(slotCount))
|
|
MTH_ST_TraceRecord("MTH_ST_Scan", "scan", "begin reason=" .. tostring(reason or "") .. " slots=" .. tostring(slotCount))
|
|
local store = MTH_ST_GetStore()
|
|
local petsRoot = MTH_PETS_GetRootStore()
|
|
MTH_PETS_RefreshCurrentPet()
|
|
local scanContext = MTH_PETS_CaptureContext()
|
|
local scanStableMasterName = MTH_PETS_GetStableMasterName()
|
|
store.lastScan = time()
|
|
store.lastEvent = tostring(reason or "manual")
|
|
store.slotCount = slotCount
|
|
store.zone = scanContext and scanContext.zone or store.zone
|
|
store.subZone = scanContext and scanContext.subZone or store.subZone
|
|
if scanStableMasterName and scanStableMasterName ~= "" then
|
|
store.stableMasterName = scanStableMasterName
|
|
end
|
|
store.slots = {}
|
|
|
|
for slot = 1, slotCount do
|
|
local r1, r2, r3, r4, r5, r6, r7, r8 = GetStablePetInfo(slot)
|
|
local slotName = tostring(r2 or "-")
|
|
local slotLevel = tostring(tonumber(r3) or "-")
|
|
local slotFamily = tostring(r4 or "-")
|
|
table.insert(slotDiagParts, "s" .. tostring(slot) .. "=" .. slotName .. "(lvl=" .. slotLevel .. ",fam=" .. slotFamily .. ")")
|
|
MTH_PETS_RecordStableSlot(petsRoot, slot, r1, r2, r3, r4, r5, r6, r7, r8, scanContext, scanStableMasterName or store.stableMasterName)
|
|
local petId = nil
|
|
if type(petsRoot) == "table" and type(petsRoot.petStore) == "table" and type(petsRoot.petStore.stableSlotIndex) == "table" then
|
|
petId = petsRoot.petStore.stableSlotIndex[slot]
|
|
end
|
|
store.slots[slot] = {
|
|
petId = petId,
|
|
icon = r1,
|
|
name = r2,
|
|
level = r3,
|
|
family = r4,
|
|
loyalty = r5,
|
|
raw = { r1, r2, r3, r4, r5, r6, r7, r8 },
|
|
r1, r2, r3, r4, r5, r6, r7, r8,
|
|
}
|
|
end
|
|
|
|
local c1, c2, c3, c4, c5, c6, c7, c8 = GetStablePetInfo(0)
|
|
local currentPetId = (type(petsRoot) == "table" and type(petsRoot.currentPet) == "table") and petsRoot.currentPet.id or nil
|
|
if currentPetId and type(petsRoot.petStore) == "table" and type(petsRoot.petStore.activeById) == "table" then
|
|
local currentRow = petsRoot.petStore.activeById[currentPetId]
|
|
if type(currentRow) == "table" then
|
|
local currentIcon = tostring(c1 or "")
|
|
if currentIcon ~= "" then
|
|
currentRow.icon = currentIcon
|
|
currentRow.lastUpdated = time()
|
|
end
|
|
end
|
|
end
|
|
store.current = {
|
|
petId = currentPetId,
|
|
icon = c1,
|
|
name = c2,
|
|
level = c3,
|
|
family = c4,
|
|
loyalty = c5,
|
|
raw = { c1, c2, c3, c4, c5, c6, c7, c8 },
|
|
}
|
|
MTH_ST_PrintStableDiag("scan current name=" .. tostring(c2 or "-")
|
|
.. " lvl=" .. tostring(tonumber(c3) or "-")
|
|
.. " fam=" .. tostring(c4 or "-")
|
|
.. " petId=" .. tostring(currentPetId or "-"))
|
|
if table.getn(slotDiagParts) > 0 then
|
|
MTH_ST_PrintStableDiag("scan slots " .. table.concat(slotDiagParts, " | "))
|
|
end
|
|
|
|
MTH_PETS_MarkStableVisited(petsRoot, tostring(reason or "stable-scan"))
|
|
|
|
MTH_PETS_RefreshCurrentPet()
|
|
|
|
local elapsedMs = nil
|
|
local finishedAt = MTH_ST_ProfileNow()
|
|
if startedAt and finishedAt then
|
|
elapsedMs = (finishedAt - startedAt) * 1000
|
|
end
|
|
if elapsedMs then
|
|
MTH_ST_TraceRecord("MTH_ST_Scan", "scan", "done reason=" .. tostring(reason or "")
|
|
.. " slots=" .. tostring(slotCount)
|
|
.. " elapsedMs=" .. string.format("%.2f", elapsedMs))
|
|
MTH_ST_PrintStableDiag("scan done ok=true slots=" .. tostring(slotCount) .. " elapsedMs=" .. string.format("%.2f", elapsedMs))
|
|
else
|
|
MTH_ST_TraceRecord("MTH_ST_Scan", "scan", "done reason=" .. tostring(reason or "")
|
|
.. " slots=" .. tostring(slotCount))
|
|
MTH_ST_PrintStableDiag("scan done ok=true slots=" .. tostring(slotCount))
|
|
end
|
|
|
|
MTH_PETS_EmitLiveState("STABLE_SCAN:" .. tostring(reason or "stable"), false)
|
|
|
|
return true
|
|
end
|
|
|
|
function MTH_GetStableScanSnapshot()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if not pets then
|
|
return nil
|
|
end
|
|
if type(pets.stableScan) ~= "table" then
|
|
return nil
|
|
end
|
|
MTH_CharSavedVariables.stableScan = pets.stableScan
|
|
return pets.stableScan
|
|
end
|
|
|
|
function MTH_RunStableScan(reason)
|
|
if type(MTH_ST_Scan) ~= "function" then
|
|
return false
|
|
end
|
|
return MTH_ST_Scan(reason or "external") and true or false
|
|
end
|
|
|
|
local function MTH_ST_RunScanThrottled(reason, minIntervalSeconds)
|
|
local now = tonumber(time()) or 0
|
|
local minInterval = tonumber(minIntervalSeconds) or 0
|
|
if minInterval > 0 and (now - (MTH_ST_LastAutoScanAt or 0)) < minInterval then
|
|
MTH_ST_PrintStableDiag("scan throttled reason=" .. tostring(reason or "")
|
|
.. " min=" .. tostring(minInterval)
|
|
.. " age=" .. tostring(now - (MTH_ST_LastAutoScanAt or 0)))
|
|
MTH_ST_TraceRecord("MTH_ST_RunScanThrottled", "scan", "throttled reason=" .. tostring(reason or "")
|
|
.. " min=" .. tostring(minInterval)
|
|
.. " age=" .. tostring(now - (MTH_ST_LastAutoScanAt or 0)))
|
|
return false
|
|
end
|
|
MTH_ST_LastAutoScanAt = now
|
|
MTH_ST_PrintStableDiag("scan trigger reason=" .. tostring(reason or "") .. " min=" .. tostring(minInterval))
|
|
MTH_ST_TraceRecord("MTH_ST_RunScanThrottled", "scan", "trigger reason=" .. tostring(reason or "")
|
|
.. " min=" .. tostring(minInterval))
|
|
return MTH_ST_Scan(reason)
|
|
end
|
|
|
|
local function MTH_ST_QueueDeferredStableScan(reason, delaySeconds)
|
|
if not MTH_ST_DeferredStableScanFrame then
|
|
MTH_ST_DeferredStableScanFrame = CreateFrame("Frame", "MTH_DeferredStableScan")
|
|
if not MTH_ST_DeferredStableScanFrame then
|
|
MTH_ST_TraceRecord("MTH_ST_QueueDeferredStableScan", "scan", "frame-create-failed")
|
|
return false
|
|
end
|
|
end
|
|
|
|
local frame = MTH_ST_DeferredStableScanFrame
|
|
frame._reason = tostring(reason or "deferred")
|
|
frame._elapsed = 0
|
|
frame._delay = tonumber(delaySeconds) or 0.30
|
|
MTH_ST_PrintStableDiag("deferred queue reason=" .. tostring(frame._reason) .. " delay=" .. tostring(frame._delay))
|
|
frame:SetScript("OnUpdate", function()
|
|
this._elapsed = (this._elapsed or 0) + (arg1 or 0)
|
|
if this._elapsed < (this._delay or 0.30) then
|
|
return
|
|
end
|
|
this:SetScript("OnUpdate", nil)
|
|
MTH_ST_PrintStableDiag("deferred execute reason=" .. tostring(this._reason or "deferred"))
|
|
MTH_ST_TraceRecord("MTH_ST_QueueDeferredStableScan", "scan", "execute reason=" .. tostring(this._reason or "deferred"))
|
|
MTH_ST_RunScanThrottled("deferred:" .. tostring(this._reason or "stable"), 1)
|
|
end)
|
|
MTH_ST_TraceRecord("MTH_ST_QueueDeferredStableScan", "scan", "queued reason=" .. tostring(frame._reason)
|
|
.. " delay=" .. tostring(frame._delay))
|
|
return true
|
|
end
|
|
|
|
local function MTH_PETS_HandleUnitPetTransition(source)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return
|
|
end
|
|
MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
|
|
local hadPet = MTH_ST_LastUnitPetHadPet and true or false
|
|
local previousCurrentId = (type(pets.currentPet) == "table") and pets.currentPet.id or nil
|
|
local snapshot = MTH_PETS_MakeSnapshotFromLivePet()
|
|
if pets.currentPetSuppressed == true and pets.currentPetSuppressedAwaitNoLive == true then
|
|
if type(snapshot) == "table" then
|
|
snapshot = nil
|
|
else
|
|
pets.currentPetSuppressedAwaitNoLive = false
|
|
end
|
|
end
|
|
local hasPet = snapshot and true or false
|
|
local pending = MTH_PETS_LastTameAttempt
|
|
local pendingTarget = (type(pending) == "table" and type(pending.target) == "table") and pending.target or nil
|
|
local hasPendingTame = type(pendingTarget) == "table"
|
|
if hasPendingTame then
|
|
MTH_PETS_LogTame("UNIT_PET source=" .. tostring(source)
|
|
.. " hadPet=" .. tostring(hadPet)
|
|
.. " hasLiveSnapshot=" .. tostring(type(snapshot) == "table")
|
|
.. " pendingTarget='" .. tostring((pendingTarget and pendingTarget["name"]) or "")
|
|
.. "' pendingBeastId='" .. tostring((pendingTarget and pendingTarget["beastId"]) or "") .. "'")
|
|
end
|
|
MTH_PETS_LogConsistency("HandleUnitPetTransition source=" .. tostring(source)
|
|
.. " previousCurrentId=" .. tostring(previousCurrentId)
|
|
.. " hadPet=" .. tostring(hadPet)
|
|
.. " hasLiveSnapshot=" .. tostring(type(snapshot) == "table"))
|
|
|
|
if type(snapshot) == "table" then
|
|
local transitionSource = "unit-pet-refresh"
|
|
if not hadPet then
|
|
transitionSource = "unit-pet-acquire"
|
|
end
|
|
if not hadPet and snapshot.beastId == nil then
|
|
snapshot.beastId = MTH_PETS_GetPendingTameBeastId(snapshot)
|
|
MTH_PETS_LogTame("UNIT_PET acquire beastId resolved to " .. tostring(snapshot.beastId or "nil"))
|
|
end
|
|
|
|
local petId, created = MTH_PETS_UpsertActivePetFromSnapshot(pets, snapshot, transitionSource, nil)
|
|
MTH_PETS_SetCurrentPetSuppressed(pets, false)
|
|
local cp = pets.currentPet
|
|
cp.exists = true
|
|
cp.id = petId
|
|
cp.guid = snapshot.guid
|
|
cp.signature = snapshot.signature
|
|
cp.beastId = snapshot.beastId
|
|
cp.name = snapshot.name
|
|
cp.family = snapshot.family
|
|
cp.level = snapshot.level
|
|
cp.happiness = snapshot.happiness
|
|
cp.loyalty = snapshot.loyalty
|
|
cp.loyaltyLevel = snapshot.loyaltyLevel
|
|
cp.xp = snapshot.xp
|
|
cp.xpMax = snapshot.xpMax
|
|
cp.xpPercent = snapshot.xpPercent
|
|
cp.lastUpdated = time()
|
|
cp.lastSeen = time()
|
|
cp.lastSource = source or transitionSource
|
|
if not hadPet or created then
|
|
cp.acquiredAt = time()
|
|
end
|
|
if cp.id and cp.id ~= previousCurrentId then
|
|
MTH_PETS_RequestPetSpellScan("unit-pet-current-change")
|
|
end
|
|
|
|
local row = MTH_PETS_GetPetById(pets, petId)
|
|
if type(row) == "table" then
|
|
if type(row.events) ~= "table" then
|
|
row.events = {}
|
|
end
|
|
local eventType = (not hadPet) and "pet-acquired" or "pet-updated"
|
|
-- Only record lifecycle events (acquire, rename, abandon), not routine updates
|
|
if eventType ~= "pet-updated" then
|
|
table.insert(row.events, {
|
|
type = eventType,
|
|
source = source or transitionSource,
|
|
at = time(),
|
|
context = MTH_PETS_CaptureContext(),
|
|
})
|
|
end
|
|
end
|
|
if not hadPet and hasPendingTame then
|
|
MTH_PETS_LogTame("Acquire completed; clearing pending tame attempt")
|
|
MTH_PETS_LastTameAttempt = nil
|
|
end
|
|
else
|
|
local cp = pets.currentPet
|
|
cp.exists = false
|
|
local keepCurrentId, keepCurrentRow = MTH_PETS_ResolveNoLiveCurrentRow(pets, previousCurrentId)
|
|
if keepCurrentId ~= nil and type(keepCurrentRow) == "table" then
|
|
MTH_PETS_ApplyCurrentPetFromActiveRow(cp, keepCurrentId, keepCurrentRow)
|
|
else
|
|
cp.id = nil
|
|
cp.guid = nil
|
|
cp.signature = nil
|
|
cp.beastId = nil
|
|
cp.name = nil
|
|
cp.family = nil
|
|
cp.level = nil
|
|
cp.happiness = nil
|
|
cp.loyalty = nil
|
|
cp.loyaltyLevel = nil
|
|
cp.xp = nil
|
|
cp.xpMax = nil
|
|
cp.xpPercent = nil
|
|
end
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = source or "unit-pet-none"
|
|
MTH_PETS_SetCurrentPetId(pets, keepCurrentId)
|
|
MTH_PETS_LogConsistency("HandleUnitPetTransition no-live result current=" .. MTH_PETS_FormatCurrentConsistency(cp)
|
|
.. " activeCurrentId=" .. tostring(type(pets.petStore) == "table" and pets.petStore.activeCurrentId or nil))
|
|
if hasPendingTame then
|
|
MTH_PETS_LogTame("UNIT_PET no-live while tame pending; keeping pending attempt")
|
|
end
|
|
end
|
|
|
|
MTH_ST_LastUnitPetHadPet = hasPet
|
|
pets.updatedAt = time()
|
|
end
|
|
|
|
function MTH_GetStablePetNames()
|
|
MTH_PETS_RefreshCurrentPet()
|
|
local names = {}
|
|
local seen = {}
|
|
local snapshot = MTH_GetStableScanSnapshot()
|
|
|
|
if snapshot and type(snapshot.slots) == "table" then
|
|
for _, row in pairs(snapshot.slots) do
|
|
local petName = nil
|
|
if type(row) == "table" then
|
|
petName = row.name or row[2]
|
|
end
|
|
if petName and petName ~= "" and not seen[petName] then
|
|
seen[petName] = true
|
|
table.insert(names, petName)
|
|
end
|
|
end
|
|
end
|
|
|
|
if type(UnitName) == "function" then
|
|
local activePetName = UnitName("pet")
|
|
if activePetName and activePetName ~= "" and not seen[activePetName] then
|
|
seen[activePetName] = true
|
|
table.insert(names, activePetName)
|
|
end
|
|
end
|
|
|
|
return names
|
|
end
|
|
|
|
local function MTH_ST_CountStableSlots(snapshot)
|
|
if type(snapshot) ~= "table" then
|
|
return 0
|
|
end
|
|
local count = tonumber(snapshot.slotCount) or 0
|
|
if count > 0 then
|
|
return count
|
|
end
|
|
if type(snapshot.slots) ~= "table" then
|
|
return 0
|
|
end
|
|
for _ in pairs(snapshot.slots) do
|
|
count = count + 1
|
|
end
|
|
return count
|
|
end
|
|
|
|
local function MTH_ST_DebugDumpLine(line)
|
|
if MTH and MTH.Print then
|
|
MTH:Print(tostring(line or ""), "debug")
|
|
end
|
|
end
|
|
|
|
local MTH_PETS_DebugBaselineSnapshot = nil
|
|
|
|
local function MTH_ST_DebugCloneValue(value, seen)
|
|
if type(value) ~= "table" then
|
|
return value
|
|
end
|
|
seen = seen or {}
|
|
if seen[value] then
|
|
return seen[value]
|
|
end
|
|
local clone = {}
|
|
seen[value] = clone
|
|
for key, child in pairs(value) do
|
|
clone[MTH_ST_DebugCloneValue(key, seen)] = MTH_ST_DebugCloneValue(child, seen)
|
|
end
|
|
return clone
|
|
end
|
|
|
|
local function MTH_PETS_CaptureDebugSnapshot(targetPetId)
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
return nil, "pets datastore unavailable"
|
|
end
|
|
|
|
MTH_PETS_RefreshCurrentPet()
|
|
|
|
local cp = (type(pets.currentPet) == "table") and pets.currentPet or nil
|
|
local petId = targetPetId or (cp and cp.id) or pets.currentPetId
|
|
if petId == nil or tostring(petId) == "" then
|
|
return nil, "no current pet id"
|
|
end
|
|
|
|
local row = MTH_PETS_GetPetById(pets, petId)
|
|
if type(row) ~= "table" then
|
|
return nil, "active row missing for pet id=" .. tostring(petId)
|
|
end
|
|
|
|
local snapshot = {
|
|
capturedAt = time(),
|
|
petId = tostring(petId),
|
|
currentPetId = pets.currentPetId,
|
|
currentPet = MTH_ST_DebugCloneValue(cp or {}),
|
|
row = MTH_ST_DebugCloneValue(row),
|
|
stableScan = MTH_ST_DebugCloneValue(pets.stableScan or {}),
|
|
}
|
|
return snapshot, nil
|
|
end
|
|
|
|
local function MTH_ST_DebugSortKeys(tbl)
|
|
local keys = {}
|
|
for k in pairs(tbl or {}) do
|
|
table.insert(keys, k)
|
|
end
|
|
table.sort(keys, function(a, b)
|
|
return tostring(a) < tostring(b)
|
|
end)
|
|
return keys
|
|
end
|
|
|
|
local function MTH_ST_DebugDumpValue(prefix, value, depth, maxDepth, seen)
|
|
depth = tonumber(depth) or 0
|
|
maxDepth = tonumber(maxDepth) or 4
|
|
seen = seen or {}
|
|
|
|
if type(value) ~= "table" then
|
|
MTH_ST_DebugDumpLine(prefix .. " = " .. tostring(value))
|
|
return
|
|
end
|
|
|
|
if seen[value] then
|
|
MTH_ST_DebugDumpLine(prefix .. " = <cycle>")
|
|
return
|
|
end
|
|
seen[value] = true
|
|
|
|
if depth >= maxDepth then
|
|
MTH_ST_DebugDumpLine(prefix .. " = <max-depth>")
|
|
return
|
|
end
|
|
|
|
local keys = MTH_ST_DebugSortKeys(value)
|
|
if table.getn(keys) == 0 then
|
|
MTH_ST_DebugDumpLine(prefix .. " = {}")
|
|
return
|
|
end
|
|
|
|
for i = 1, table.getn(keys) do
|
|
local key = keys[i]
|
|
local child = value[key]
|
|
local childPrefix = prefix .. "." .. tostring(key)
|
|
if type(child) == "table" then
|
|
MTH_ST_DebugDumpLine(childPrefix .. " = {")
|
|
MTH_ST_DebugDumpValue(childPrefix, child, depth + 1, maxDepth, seen)
|
|
MTH_ST_DebugDumpLine(childPrefix .. " = }")
|
|
else
|
|
MTH_ST_DebugDumpLine(childPrefix .. " = " .. tostring(child))
|
|
end
|
|
end
|
|
end
|
|
|
|
function MTH_CommandPetsState()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
MTH_PETS_RefreshCurrentPet()
|
|
|
|
local currentPet = pets and pets.currentPet or nil
|
|
local stableScan = pets and pets.stableScan or nil
|
|
local petTraining = pets and (pets.trainScan or pets.petTraining) or nil
|
|
local petStore = pets and pets.petStore or nil
|
|
local activeCount = 0
|
|
local historyCount = 0
|
|
if type(petStore) == "table" and type(petStore.activeById) == "table" then
|
|
for _ in pairs(petStore.activeById) do activeCount = activeCount + 1 end
|
|
end
|
|
if type(petStore) == "table" and type(petStore.historyById) == "table" then
|
|
for _ in pairs(petStore.historyById) do historyCount = historyCount + 1 end
|
|
end
|
|
|
|
local currentPetText = "none"
|
|
if type(currentPet) == "table" and currentPet.exists then
|
|
local petName = tostring(currentPet.name or "")
|
|
local petLevel = tonumber(currentPet.level) or 0
|
|
if petName ~= "" then
|
|
if petLevel > 0 then
|
|
currentPetText = petName .. " (" .. tostring(petLevel) .. ")"
|
|
else
|
|
currentPetText = petName
|
|
end
|
|
end
|
|
end
|
|
|
|
local summary = "MTH_Pets: schemaVersion=" .. tostring(pets and pets.schemaVersion or 0)
|
|
.. " currentPet=" .. currentPetText
|
|
.. " activePets=" .. tostring(activeCount)
|
|
.. " petHistory=" .. tostring(historyCount)
|
|
.. " stableSlots=" .. tostring(MTH_ST_CountStableSlots(stableScan))
|
|
.. " stableLastScan=" .. tostring(stableScan and stableScan.lastScan or 0)
|
|
.. " petTrainingLastScan=" .. tostring(petTraining and petTraining.lastScan or 0)
|
|
.. " hasCompletedPetScan=" .. tostring(petTraining and petTraining.hasCompletedPetScan or nil)
|
|
MTH:Print(summary)
|
|
MTH:Print("[PETSSTATE] " .. summary, "debug")
|
|
end
|
|
|
|
function MTH_CommandPetsReset()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
if type(pets) ~= "table" then
|
|
MTH:Print("Pets reset failed: datastore unavailable.")
|
|
return false
|
|
end
|
|
|
|
MTH_PETS_EnsureSchema(pets)
|
|
local store = MTH_PETS_GetStoreTables(pets)
|
|
if type(store) ~= "table" then
|
|
MTH:Print("Pets reset failed: petStore unavailable.")
|
|
return false
|
|
end
|
|
|
|
local activeCount = 0
|
|
if type(store.activeById) == "table" then
|
|
for _ in pairs(store.activeById) do
|
|
activeCount = activeCount + 1
|
|
end
|
|
end
|
|
|
|
local historyCount = 0
|
|
if type(store.historyById) == "table" then
|
|
for _ in pairs(store.historyById) do
|
|
historyCount = historyCount + 1
|
|
end
|
|
else
|
|
store.historyById = {}
|
|
end
|
|
|
|
store.activeById = {}
|
|
store.signatureIndex = {}
|
|
store.guidIndex = {}
|
|
store.stableSlotIndex = {}
|
|
store.activeCurrentId = nil
|
|
pets.currentPetId = nil
|
|
pets.currentPetSuppressed = false
|
|
pets.currentPetSuppressedReason = ""
|
|
store.nextId = 1
|
|
|
|
MTH_PETS_EnsureCurrentPetSchema(pets)
|
|
local cp = pets.currentPet
|
|
cp.exists = false
|
|
cp.id = nil
|
|
cp.guid = nil
|
|
cp.signature = nil
|
|
cp.beastId = nil
|
|
cp.name = nil
|
|
cp.family = nil
|
|
cp.level = nil
|
|
cp.happiness = nil
|
|
cp.loyalty = nil
|
|
cp.loyaltyLevel = nil
|
|
cp.xp = nil
|
|
cp.xpMax = nil
|
|
cp.xpPercent = nil
|
|
cp.acquiredAt = 0
|
|
cp.lastSeen = time()
|
|
cp.lastUpdated = time()
|
|
cp.lastSource = "pets-reset"
|
|
|
|
if type(pets.stableScan) ~= "table" then
|
|
pets.stableScan = {}
|
|
end
|
|
pets.stableScan.lastScan = 0
|
|
pets.stableScan.lastEvent = "reset:/mth pets reset"
|
|
pets.stableScan.slotCount = 0
|
|
pets.stableScan.slots = {}
|
|
|
|
MTH_ST_LastUnitPetHadPet = false
|
|
pets.updatedAt = time()
|
|
|
|
if type(MTH_CharSavedVariables) == "table" then
|
|
MTH_CharSavedVariables.MTH_Pets = pets
|
|
MTH_CharSavedVariables.petStore = pets.petStore
|
|
MTH_CharSavedVariables.stableScan = pets.stableScan
|
|
end
|
|
|
|
local line = "Pets reset complete: clearedActive=" .. tostring(activeCount)
|
|
.. " preservedHistory=" .. tostring(historyCount)
|
|
.. " nextId=" .. tostring(store.nextId)
|
|
MTH:Print(line)
|
|
MTH:Print("[PETSRESET] " .. line, "debug")
|
|
return true
|
|
end
|
|
|
|
function MTH_CommandPetsDump()
|
|
local pets = MTH_PETS_GetRootStore()
|
|
MTH_PETS_RefreshCurrentPet()
|
|
|
|
if type(pets) ~= "table" or type(pets.currentPet) ~= "table" or not pets.currentPet.exists or not pets.currentPet.id then
|
|
MTH:Print("Current pet dump: no active pet.")
|
|
return
|
|
end
|
|
|
|
local cp = pets.currentPet
|
|
local row = nil
|
|
if type(pets.petStore) == "table" and type(pets.petStore.activeById) == "table" then
|
|
row = pets.petStore.activeById[cp.id]
|
|
end
|
|
if type(row) ~= "table" then
|
|
MTH:Print("Current pet dump: active row missing for id=" .. tostring(cp.id))
|
|
return
|
|
end
|
|
|
|
MTH_ST_DebugDumpLine("[PETDUMP] BEGIN currentPet")
|
|
MTH_ST_DebugDumpValue("[PETDUMP].currentPet", cp, 0, 3, {})
|
|
MTH_ST_DebugDumpLine("[PETDUMP] END currentPet")
|
|
|
|
MTH_ST_DebugDumpLine("[PETDUMP] BEGIN activeRow[" .. tostring(cp.id) .. "]")
|
|
MTH_ST_DebugDumpValue("[PETDUMP].activeRow", row, 0, 5, {})
|
|
MTH_ST_DebugDumpLine("[PETDUMP] END activeRow")
|
|
|
|
local petStore = pets.petStore
|
|
if type(petStore) == "table" and type(petStore.stableSlotIndex) == "table" then
|
|
local slotKeys = MTH_ST_DebugSortKeys(petStore.stableSlotIndex)
|
|
for i = 1, table.getn(slotKeys) do
|
|
local slot = slotKeys[i]
|
|
local petId = petStore.stableSlotIndex[slot]
|
|
MTH_ST_DebugDumpLine("[PETDUMP] stableSlotIndex." .. tostring(slot) .. " = " .. tostring(petId))
|
|
if petId and type(petStore.activeById) == "table" and type(petStore.activeById[petId]) == "table" then
|
|
MTH_ST_DebugDumpLine("[PETDUMP] BEGIN stableRow slot=" .. tostring(slot) .. " id=" .. tostring(petId))
|
|
MTH_ST_DebugDumpValue("[PETDUMP].stableRow[" .. tostring(slot) .. "]", petStore.activeById[petId], 0, 5, {})
|
|
MTH_ST_DebugDumpLine("[PETDUMP] END stableRow slot=" .. tostring(slot))
|
|
end
|
|
end
|
|
end
|
|
|
|
if type(pets.stableScan) == "table" then
|
|
MTH_ST_DebugDumpLine("[PETDUMP] BEGIN stableScan")
|
|
MTH_ST_DebugDumpValue("[PETDUMP].stableScan", pets.stableScan, 0, 4, {})
|
|
MTH_ST_DebugDumpLine("[PETDUMP] END stableScan")
|
|
end
|
|
end
|
|
|
|
function MTH_CommandPetsSnap()
|
|
local snapshot, err = MTH_PETS_CaptureDebugSnapshot(nil)
|
|
if not snapshot then
|
|
MTH:Print("Pet snapshot failed: " .. tostring(err or "unknown"))
|
|
return false
|
|
end
|
|
MTH_PETS_DebugBaselineSnapshot = snapshot
|
|
MTH:Print("Pet snapshot saved for id=" .. tostring(snapshot.petId) .. ". Run /mth petsdiff after reproducing the issue.")
|
|
MTH_ST_DebugDumpLine("[PETSNAP] BASELINE BEGIN")
|
|
MTH_ST_DebugDumpValue("[PETSNAP].baseline", snapshot, 0, 5, {})
|
|
MTH_ST_DebugDumpLine("[PETSNAP] BASELINE END")
|
|
return true
|
|
end
|
|
|
|
function MTH_CommandPetsDiff()
|
|
if type(MTH_PETS_DebugBaselineSnapshot) ~= "table" then
|
|
MTH:Print("No pet snapshot baseline found. Run /mth petssnap first.")
|
|
return false
|
|
end
|
|
|
|
local baseline = MTH_PETS_DebugBaselineSnapshot
|
|
local current, err = MTH_PETS_CaptureDebugSnapshot(baseline.petId)
|
|
if not current then
|
|
MTH:Print("Pet snapshot compare failed: " .. tostring(err or "unknown"))
|
|
return false
|
|
end
|
|
|
|
MTH:Print("Pet snapshot compare for id=" .. tostring(current.petId) .. ".")
|
|
MTH_ST_DebugDumpLine("[PETSNAP] BEFORE BEGIN")
|
|
MTH_ST_DebugDumpValue("[PETSNAP].before", baseline, 0, 5, {})
|
|
MTH_ST_DebugDumpLine("[PETSNAP] BEFORE END")
|
|
MTH_ST_DebugDumpLine("[PETSNAP] AFTER BEGIN")
|
|
MTH_ST_DebugDumpValue("[PETSNAP].after", current, 0, 5, {})
|
|
MTH_ST_DebugDumpLine("[PETSNAP] AFTER END")
|
|
|
|
local diffKeys = {
|
|
"name", "family", "level", "beastId", "icon",
|
|
"tameRecorded", "tamedAt", "tameZone", "tameSubZone", "tameX", "tameY", "tameBeastId",
|
|
"stableSlot", "stabledAt", "stableFirstSeenAt", "lastUnstabledAt", "lastSource", "lastUpdated",
|
|
}
|
|
local diffCount = 0
|
|
for i = 1, table.getn(diffKeys) do
|
|
local key = diffKeys[i]
|
|
local beforeValue = baseline.row and baseline.row[key] or nil
|
|
local afterValue = current.row and current.row[key] or nil
|
|
if tostring(beforeValue) ~= tostring(afterValue) then
|
|
diffCount = diffCount + 1
|
|
MTH_ST_DebugDumpLine("[PETSNAP] DIFF row." .. tostring(key) .. ": " .. tostring(beforeValue) .. " -> " .. tostring(afterValue))
|
|
end
|
|
end
|
|
if diffCount <= 0 then
|
|
MTH_ST_DebugDumpLine("[PETSNAP] DIFF none")
|
|
end
|
|
|
|
MTH_PETS_DebugBaselineSnapshot = current
|
|
return true
|
|
end
|
|
|
|
function MTH_CommandStableScan()
|
|
if UnitClass then
|
|
local _, classToken = UnitClass("player")
|
|
if classToken ~= "HUNTER" then
|
|
MTH:Print("Stable scan is hunter-only.")
|
|
return
|
|
end
|
|
end
|
|
|
|
local ok = MTH_ST_Scan("manual:/mth stablescan")
|
|
if not ok then
|
|
MTH:Print("Stable scan failed or no stable data available.")
|
|
return
|
|
end
|
|
|
|
local store = MTH_CharSavedVariables and MTH_CharSavedVariables.stableScan
|
|
local slotCount = store and store.slotCount or 0
|
|
MTH:Print("Stable scan captured " .. tostring(slotCount) .. " slot(s).")
|
|
end
|
|
|
|
function MTH_ST_HandlePlayerEnteringWorld(evt)
|
|
if evt ~= "PLAYER_ENTERING_WORLD" then
|
|
return false
|
|
end
|
|
local pets = MTH_PETS_GetRootStore()
|
|
MTH_PETS_MaybePromptStableVisit(pets)
|
|
MTH_PETS_EmitLiveState("PLAYER_ENTERING_WORLD", true)
|
|
return true
|
|
end
|
|
|
|
function MTH_ST_HandleSpellcastEvent(evt, eventArg1, eventArg2)
|
|
local isUnitSpellcast = (evt == "UNIT_SPELLCAST_START" or evt == "UNIT_SPELLCAST_STOP"
|
|
or evt == "UNIT_SPELLCAST_FAILED" or evt == "UNIT_SPELLCAST_INTERRUPTED"
|
|
or evt == "UNIT_SPELLCAST_CHANNEL_START" or evt == "UNIT_SPELLCAST_CHANNEL_STOP")
|
|
local castSpellName = eventArg1
|
|
local hasPendingTame = type(MTH_PETS_LastTameAttempt) == "table"
|
|
if isUnitSpellcast then
|
|
if eventArg1 ~= "player" then
|
|
return true
|
|
end
|
|
castSpellName = eventArg2
|
|
end
|
|
|
|
if evt == "SPELLCAST_START" or evt == "SPELLCAST_CHANNEL_START"
|
|
or evt == "UNIT_SPELLCAST_START" or evt == "UNIT_SPELLCAST_CHANNEL_START" then
|
|
if not MTH_PETS_IsTameBeastSpellName(castSpellName) then
|
|
return true
|
|
end
|
|
MTH_PETS_RecordTameAttempt(evt, castSpellName)
|
|
return true
|
|
end
|
|
|
|
if evt == "SPELLCAST_STOP" or evt == "SPELLCAST_CHANNEL_STOP" or evt == "SPELLCAST_FAILED" or evt == "SPELLCAST_INTERRUPTED"
|
|
or evt == "UNIT_SPELLCAST_STOP" or evt == "UNIT_SPELLCAST_CHANNEL_STOP"
|
|
or evt == "UNIT_SPELLCAST_FAILED" or evt == "UNIT_SPELLCAST_INTERRUPTED" then
|
|
local terminalIsTame = MTH_PETS_IsTameBeastSpellName(castSpellName)
|
|
if not terminalIsTame and hasPendingTame then
|
|
terminalIsTame = true
|
|
end
|
|
if not terminalIsTame then
|
|
return true
|
|
end
|
|
if terminalIsTame then
|
|
if evt == "SPELLCAST_FAILED" or evt == "SPELLCAST_INTERRUPTED" then
|
|
MTH_PETS_LastTameAttempt = nil
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function MTH_ST_HandleUnitPetEvent(evt, eventArg1)
|
|
if evt ~= "UNIT_PET" then
|
|
return false
|
|
end
|
|
if eventArg1 ~= "player" then
|
|
return true
|
|
end
|
|
|
|
local startedAt = MTH_ST_ProfileNow()
|
|
local beforeId = nil
|
|
local beforeName = nil
|
|
local beforeFamily = nil
|
|
local beforeLevel = nil
|
|
local beforeExists = nil
|
|
local petsBefore = type(MTH_PETS_GetRootStore) == "function" and MTH_PETS_GetRootStore() or nil
|
|
if type(petsBefore) == "table" and type(petsBefore.currentPet) == "table" then
|
|
beforeId = petsBefore.currentPet.id
|
|
beforeName = petsBefore.currentPet.name
|
|
beforeFamily = petsBefore.currentPet.family
|
|
beforeLevel = petsBefore.currentPet.level
|
|
beforeExists = petsBefore.currentPet.exists and true or false
|
|
end
|
|
MTH_ST_PrintStableDiag("swap UNIT_PET begin beforeId=" .. tostring(beforeId or "-") .. " beforeName=" .. tostring(beforeName or "-"))
|
|
|
|
MTH_PETS_HandleUnitPetTransition("UNIT_PET")
|
|
|
|
local afterId = nil
|
|
local afterName = nil
|
|
local afterFamily = nil
|
|
local afterLevel = nil
|
|
local afterExists = nil
|
|
local petsAfter = type(MTH_PETS_GetRootStore) == "function" and MTH_PETS_GetRootStore() or nil
|
|
if type(petsAfter) == "table" and type(petsAfter.currentPet) == "table" then
|
|
afterId = petsAfter.currentPet.id
|
|
afterName = petsAfter.currentPet.name
|
|
afterFamily = petsAfter.currentPet.family
|
|
afterLevel = petsAfter.currentPet.level
|
|
afterExists = petsAfter.currentPet.exists and true or false
|
|
end
|
|
|
|
local sameCore = (tostring(beforeId or "") == tostring(afterId or ""))
|
|
and (tostring(beforeName or "") == tostring(afterName or ""))
|
|
and (tostring(beforeFamily or "") == tostring(afterFamily or ""))
|
|
and (tonumber(beforeLevel) or -1) == (tonumber(afterLevel) or -1)
|
|
and (beforeExists and true or false) == (afterExists and true or false)
|
|
|
|
local emitSkipped = false
|
|
local liveChanged = false
|
|
if MTH_ST_IsStableFrameOpen() then
|
|
emitSkipped = true
|
|
MTH_ST_PrintStableDiag("swap UNIT_PET emit-skip reason=stable-open")
|
|
elseif sameCore then
|
|
emitSkipped = true
|
|
MTH_ST_PrintStableDiag("swap UNIT_PET emit-skip reason=unchanged-core")
|
|
else
|
|
liveChanged = MTH_PETS_EmitLiveState("UNIT_PET", false, { skipRefresh = true }) and true or false
|
|
end
|
|
|
|
local elapsedMs = 0
|
|
local finishedAt = MTH_ST_ProfileNow()
|
|
if startedAt and finishedAt then
|
|
elapsedMs = math.floor(math.max(0, (finishedAt - startedAt) * 1000) + 0.5)
|
|
end
|
|
MTH_ST_PrintStableDiag("swap UNIT_PET end afterId=" .. tostring(afterId or "-")
|
|
.. " afterName=" .. tostring(afterName or "-")
|
|
.. " sameCore=" .. tostring(sameCore)
|
|
.. " emitSkipped=" .. tostring(emitSkipped)
|
|
.. " liveChanged=" .. tostring(liveChanged)
|
|
.. " elapsedMs=" .. tostring(elapsedMs))
|
|
return true
|
|
end
|
|
|
|
local function MTH_ST_IsPlayerDeadOrGhost()
|
|
if type(UnitIsDeadOrGhost) == "function" then
|
|
return UnitIsDeadOrGhost("player") and true or false
|
|
end
|
|
if type(UnitIsDead) == "function" then
|
|
return UnitIsDead("player") and true or false
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MTH_ST_HandleVitalsEvent(evt, eventArg1)
|
|
local playerDead = MTH_ST_IsPlayerDeadOrGhost()
|
|
local hasLivePet = (type(UnitExists) == "function" and UnitExists("pet")) and true or false
|
|
|
|
if evt == "PET_BAR_UPDATE" then
|
|
if not hasLivePet then
|
|
MTH_ST_TraceRecord(evt, "vitals", "pet_bar:no-pet")
|
|
return true
|
|
end
|
|
local interval = playerDead and 1.75 or 0.5
|
|
if not MTH_PETS_ShouldHandleThrottledEvent("PET_BAR_UPDATE", interval) then
|
|
MTH_ST_TraceRecord(evt, "vitals", "pet_bar:throttled")
|
|
return true
|
|
end
|
|
local changed = MTH_PETS_ApplyLivePetVitalsFast("PET_BAR_UPDATE")
|
|
if changed then
|
|
MTH_ST_TraceRecord(evt, "vitals", "pet_bar:changed")
|
|
MTH_PETS_EmitLiveState("PET_BAR_UPDATE", false, { skipRefresh = true })
|
|
else
|
|
MTH_ST_TraceRecord(evt, "vitals", "pet_bar:no-change")
|
|
end
|
|
return true
|
|
end
|
|
|
|
if evt == "UNIT_HAPPINESS" then
|
|
if not hasLivePet then
|
|
MTH_ST_TraceRecord(evt, "vitals", "happiness:no-pet")
|
|
return true
|
|
end
|
|
if eventArg1 and eventArg1 ~= "pet" then
|
|
MTH_ST_TraceRecord(evt, "vitals", "happiness:not-pet-arg")
|
|
return true
|
|
end
|
|
local interval = playerDead and 2.25 or 0.75
|
|
if not MTH_PETS_ShouldHandleThrottledEvent("UNIT_HAPPINESS", interval) then
|
|
MTH_ST_TraceRecord(evt, "vitals", "happiness:throttled")
|
|
return true
|
|
end
|
|
local changed = MTH_PETS_ApplyLivePetVitalsFast("UNIT_HAPPINESS")
|
|
if changed then
|
|
MTH_ST_TraceRecord(evt, "vitals", "happiness:changed")
|
|
MTH_PETS_EmitLiveState("UNIT_HAPPINESS", false, { skipRefresh = true })
|
|
else
|
|
MTH_ST_TraceRecord(evt, "vitals", "happiness:no-change")
|
|
end
|
|
return true
|
|
end
|
|
|
|
if evt == "CHAT_MSG_COMBAT_XP_GAIN" or evt == "PLAYER_XP_UPDATE" then
|
|
if not hasLivePet then
|
|
MTH_ST_TraceRecord(evt, "vitals", "xp:no-pet")
|
|
return true
|
|
end
|
|
if playerDead then
|
|
MTH_ST_TraceRecord(evt, "vitals", "xp:dead")
|
|
return true
|
|
end
|
|
if not MTH_PETS_ShouldHandleThrottledEvent("XP_UPDATE", 1.5) then
|
|
MTH_ST_TraceRecord(evt, "vitals", "xp:throttled")
|
|
return true
|
|
end
|
|
local changed = MTH_PETS_ApplyLivePetVitalsFast(evt)
|
|
if changed then
|
|
MTH_ST_TraceRecord(evt, "vitals", "xp:changed")
|
|
MTH_PETS_EmitLiveState(evt, false, { skipRefresh = true })
|
|
else
|
|
MTH_ST_TraceRecord(evt, "vitals", "xp:no-change")
|
|
end
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function MTH_ST_HandleRunawayEvent(evt, eventArg1)
|
|
if evt ~= "CHAT_MSG_SYSTEM" and evt ~= "UI_ERROR_MESSAGE"
|
|
and evt ~= "CHAT_MSG_SPELL_PET_INFO" and evt ~= "CHAT_MSG_SPELL_PET_DAMAGE"
|
|
and evt ~= "CHAT_MSG_COMBAT_PET_HITS" and evt ~= "CHAT_MSG_COMBAT_PET_MISSES" then
|
|
return false
|
|
end
|
|
|
|
if type(UnitExists) == "function" and not UnitExists("pet") then
|
|
if not MTH_PETS_ShouldHandleThrottledEvent("RUNAWAY_MSG_NO_PET", 2.0) then
|
|
MTH_ST_TraceRecord(evt, "runaway", "no-pet:throttled")
|
|
return true
|
|
end
|
|
MTH_ST_TraceRecord(evt, "runaway", "no-pet:parse")
|
|
end
|
|
|
|
if MTH_ST_IsPlayerDeadOrGhost() then
|
|
if not MTH_PETS_ShouldHandleThrottledEvent("RUNAWAY_MSG_DEAD", 0.30) then
|
|
MTH_ST_TraceRecord(evt, "runaway", "dead:throttled")
|
|
return true
|
|
end
|
|
end
|
|
|
|
local runawayStarted = MTH_ST_ProfileNow()
|
|
local matchedRunaway = MTH_PETS_IsRunawaySystemMessage(eventArg1)
|
|
if runawayStarted then
|
|
local runawayNow = MTH_ST_ProfileNow()
|
|
if runawayNow then
|
|
MTH_ST_ProfileRecordRunaway((runawayNow - runawayStarted) * 1000)
|
|
end
|
|
end
|
|
if matchedRunaway then
|
|
MTH_ST_TraceRecord(evt, "runaway", "matched")
|
|
MTH_PETS_RecordPetRunaway(evt, eventArg1)
|
|
MTH_PETS_EmitLiveState(evt)
|
|
else
|
|
MTH_ST_TraceRecord(evt, "runaway", "not-matched")
|
|
end
|
|
return true
|
|
end
|
|
|
|
function MTH_ST_HandleStableUiEvent(evt)
|
|
if evt == "PET_STABLE_SHOW" then
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "show")
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "show:no-scan")
|
|
return true
|
|
end
|
|
|
|
if evt == "PET_STABLE_UPDATE" then
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "update")
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "update:no-scan")
|
|
MTH_PETS_EmitLiveState(evt)
|
|
return true
|
|
end
|
|
|
|
if evt == "PET_STABLE_CLOSED" then
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "closed")
|
|
MTH_ST_QueueDeferredStableScan("PET_STABLE_CLOSED", 0.20)
|
|
MTH_ST_TraceRecord(evt, "stable-ui", "closed:deferred-scan")
|
|
MTH_PETS_EmitLiveState(evt)
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function MTH_ST_DispatchEvent(_, evt, eventArg1, eventArg2)
|
|
evt = evt or event
|
|
eventArg1 = eventArg1 or arg1
|
|
eventArg2 = eventArg2 or arg2
|
|
if not evt then
|
|
return
|
|
end
|
|
|
|
if type(MTH_ST_LifecycleService) == "table" and type(MTH_ST_LifecycleService.HandleEvent) == "function" then
|
|
local lifecycleHandled, lifecycleNote = MTH_ST_LifecycleService:HandleEvent(evt, eventArg1, eventArg2)
|
|
if lifecycleHandled then
|
|
MTH_ST_TraceRecord(evt, "dispatch", lifecycleNote or "lifecycle-service")
|
|
return
|
|
end
|
|
else
|
|
if MTH_ST_HandlePlayerEnteringWorld(evt) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "lifecycle-enter-world")
|
|
return
|
|
end
|
|
if MTH_ST_HandleSpellcastEvent(evt, eventArg1, eventArg2) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "lifecycle-spellcast")
|
|
return
|
|
end
|
|
if MTH_ST_HandleUnitPetEvent(evt, eventArg1) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "lifecycle-unit-pet")
|
|
return
|
|
end
|
|
end
|
|
|
|
if type(MTH_ST_VitalsService) == "table" and type(MTH_ST_VitalsService.HandleEvent) == "function" then
|
|
if MTH_ST_VitalsService:HandleEvent(evt, eventArg1, eventArg2) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "vitals-service")
|
|
return
|
|
end
|
|
else
|
|
if MTH_ST_HandleVitalsEvent(evt, eventArg1) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "vitals")
|
|
return
|
|
end
|
|
end
|
|
|
|
if type(MTH_ST_RunawayService) == "table" and type(MTH_ST_RunawayService.HandleEvent) == "function" then
|
|
if MTH_ST_RunawayService:HandleEvent(evt, eventArg1, eventArg2) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "runaway-service")
|
|
return
|
|
end
|
|
else
|
|
if MTH_ST_HandleRunawayEvent(evt, eventArg1) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "runaway")
|
|
return
|
|
end
|
|
end
|
|
|
|
if type(MTH_ST_StableScanService) == "table" and type(MTH_ST_StableScanService.HandleEvent) == "function" then
|
|
if MTH_ST_StableScanService:HandleEvent(evt, eventArg1, eventArg2) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "stable-service")
|
|
return
|
|
end
|
|
else
|
|
if MTH_ST_HandleStableUiEvent(evt) then
|
|
MTH_ST_TraceRecord(evt, "dispatch", "stable-ui")
|
|
return
|
|
end
|
|
end
|
|
|
|
MTH_ST_TraceRecord(evt, "dispatch", "unhandled")
|
|
end
|
|
|
|
local function MTH_ST_OnEvent(frame, evt, eventArg1, eventArg2)
|
|
local started = nil
|
|
if MTH_ST_Profile and MTH_ST_Profile.enabled then
|
|
started = MTH_ST_ProfileNow()
|
|
end
|
|
|
|
local stableDiagStart = nil
|
|
local isStableUiEvt = MTH_ST_IsStableUiEvent(evt)
|
|
if isStableUiEvt then
|
|
stableDiagStart = MTH_ST_ProfileNow()
|
|
MTH_ST_PrintStableDiag("evt=" .. tostring(evt) .. " phase=begin")
|
|
if MTH_ST_Trace and not MTH_ST_Trace.enabled then
|
|
MTH_ST_Trace.enabled = true
|
|
MTH_ST_Trace.verbose = false
|
|
MTH_ST_TraceResetCounters()
|
|
MTH_ST_PrintStableDiag("trace=auto-enabled")
|
|
end
|
|
end
|
|
|
|
MTH_ST_DispatchEvent(frame, evt, eventArg1, eventArg2)
|
|
|
|
if isStableUiEvt then
|
|
local stableDiagEnd = MTH_ST_ProfileNow()
|
|
local elapsedMs = 0
|
|
if stableDiagStart and stableDiagEnd then
|
|
elapsedMs = math.floor(math.max(0, (stableDiagEnd - stableDiagStart) * 1000) + 0.5)
|
|
end
|
|
MTH_ST_PrintStableDiag("evt=" .. tostring(evt) .. " phase=end elapsedMs=" .. tostring(elapsedMs))
|
|
MTH_ST_TraceFlushIfDue(true)
|
|
end
|
|
|
|
if started then
|
|
local finished = MTH_ST_ProfileNow()
|
|
if finished then
|
|
MTH_ST_ProfileRecordEvent(evt or event, (finished - started) * 1000)
|
|
MTH_ST_ProfileFlushIfDue(false)
|
|
end
|
|
end
|
|
MTH_ST_TraceFlushIfDue(false)
|
|
end
|
|
|
|
function MTH_ST_InitService()
|
|
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate("stablescan-init") then
|
|
return
|
|
end
|
|
|
|
if MTH_PetLifecycleEventFrame then
|
|
return
|
|
end
|
|
|
|
MTH_PetLifecycleEventFrame = CreateFrame("Frame", "MTH_PetLifecycleEvent")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("UNIT_PET")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PET_BAR_UPDATE")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("UNIT_HAPPINESS")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PLAYER_XP_UPDATE")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_SYSTEM")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_SPELL_PET_INFO")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_SPELL_PET_DAMAGE")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_COMBAT_PET_HITS")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_COMBAT_PET_MISSES")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("UI_ERROR_MESSAGE")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PET_STABLE_SHOW")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PET_STABLE_UPDATE")
|
|
MTH_PetLifecycleEventFrame:RegisterEvent("PET_STABLE_CLOSED")
|
|
MTH_PetLifecycleEventFrame:SetScript("OnEvent", MTH_ST_OnEvent)
|
|
MTH_ST_LastUnitPetHadPet = ((type(UnitExists) == "function") and UnitExists("pet")) and true or false
|
|
MTH_PETS_InstallCoreRenameHook()
|
|
MTH_PETS_EmitLiveState("service:init", true)
|
|
if MTH_PETS_TRACE_CONSISTENCY and MTH and MTH.Print then
|
|
MTH:Print("[PETCONSIST] trace enabled", "debug")
|
|
end
|
|
end
|
|
|
|
function MTH_ST_InitBootstrap()
|
|
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate("stablescan-bootstrap") then
|
|
return
|
|
end
|
|
if MTH_PetLifecycleEventFrame or MTH_PetLifecycleBootstrapFrame then
|
|
return
|
|
end
|
|
|
|
local frame = CreateFrame("Frame", "MTH_PetLifecycleBoot")
|
|
if not frame then
|
|
return
|
|
end
|
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
frame:RegisterEvent("PET_STABLE_SHOW")
|
|
frame:RegisterEvent("PET_STABLE_UPDATE")
|
|
frame:SetScript("OnEvent", function()
|
|
if event ~= "PLAYER_ENTERING_WORLD" and event ~= "PET_STABLE_SHOW" and event ~= "PET_STABLE_UPDATE" then
|
|
return
|
|
end
|
|
if event == "PET_STABLE_SHOW" or event == "PET_STABLE_UPDATE" then
|
|
MTH_ST_PrintStableDiag("bootstrap evt=" .. tostring(event) .. " phase=begin")
|
|
end
|
|
MTH_ST_InitService()
|
|
if event == "PET_STABLE_SHOW" or event == "PET_STABLE_UPDATE" then
|
|
MTH_ST_PrintStableDiag("bootstrap evt=" .. tostring(event) .. " phase=end action=no-scan")
|
|
end
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnEvent", nil)
|
|
MTH_PetLifecycleBootstrapFrame = nil
|
|
end)
|
|
MTH_PetLifecycleBootstrapFrame = frame
|
|
end
|
|
|
|
function MTH_ST_ShutdownService(_reason)
|
|
if not MTH_PetLifecycleEventFrame then
|
|
return
|
|
end
|
|
|
|
MTH_PetLifecycleEventFrame:UnregisterAllEvents()
|
|
MTH_PetLifecycleEventFrame:SetScript("OnEvent", nil)
|
|
MTH_PetLifecycleEventFrame = nil
|
|
MTH_ST_LastAutoScanAt = 0
|
|
MTH_ST_LastUnitPetHadPet = false
|
|
MTH_PETS_LiveState = nil
|
|
MTH_PETS_LiveStateSeq = 0
|
|
|
|
if type(PetRename) == "function" and PetRename == MTH_PETS_CorePetRenameHook and type(MTH_PETS_CoreOriginal_PetRename) == "function" then
|
|
PetRename = MTH_PETS_CoreOriginal_PetRename
|
|
end
|
|
if MTH and MTH.RestoreHookBoundary then
|
|
MTH:RestoreHookBoundary(MTH_PETS_CORE_HOOK_BOUNDARY_KEY)
|
|
end
|
|
end
|
|
|
|
MTH_PETS_GetRootStore()
|