mirror of
https://codeberg.org/Duvelcorp/MetaHunt.git
synced 2026-09-22 07:36:58 +00:00
c55cd66ffc
- Hunter's Book and options window now show a full 'DISABLED.' overlay on realm-gated realms instead of rendering content - Pet training: new rank of an already-known ability is now recognised, announced, and reflected in tooltips - Restore curated 'unique appearance' beast allowlist (34 beasts) in data/init.lua - CHANGELOG updated
993 lines
29 KiB
Lua
993 lines
29 KiB
Lua
if not MTH then
|
|
error("MetaHunt core framework missing: api/core-framework.lua must load before api/core-scan-beasttraining.lua")
|
|
end
|
|
|
|
local function MTH_PT_SafeLower(text)
|
|
if text == nil then return "" end
|
|
return string.lower(tostring(text))
|
|
end
|
|
|
|
local function MTH_PT_GetGlobal(name)
|
|
if type(getglobal) == "function" then
|
|
return getglobal(name)
|
|
end
|
|
if _G then
|
|
return _G[name]
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local MTH_PT_ApplyScan
|
|
local MTH_PT_AbilityHasPositiveRanks
|
|
local MTH_PT_PromoteUnrankedKnownAliases
|
|
local MTH_PT_BootstrapFrame = nil
|
|
|
|
local MTH_PT_VERBOSE_SCAN_LOGS = false
|
|
|
|
local function MTH_PT_GetStore()
|
|
if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end
|
|
if not MTH_CharSavedVariables.trainScan and MTH_CharSavedVariables.petTraining then
|
|
MTH_CharSavedVariables.trainScan = MTH_CharSavedVariables.petTraining
|
|
end
|
|
if not MTH_CharSavedVariables.trainScan then
|
|
MTH_CharSavedVariables.trainScan = {
|
|
spellMap = {},
|
|
hunterKnownMap = {},
|
|
lastScan = 0,
|
|
lastPrompt = 0,
|
|
hasCompletedPetScan = nil,
|
|
}
|
|
end
|
|
if not MTH_CharSavedVariables.trainScan.spellMap then
|
|
MTH_CharSavedVariables.trainScan.spellMap = {}
|
|
end
|
|
if not MTH_CharSavedVariables.trainScan.hunterKnownMap then
|
|
MTH_CharSavedVariables.trainScan.hunterKnownMap = {}
|
|
end
|
|
if MTH_CharSavedVariables.trainScan.hasCompletedPetScan == nil then
|
|
if tonumber(MTH_CharSavedVariables.trainScan.lastScan) and tonumber(MTH_CharSavedVariables.trainScan.lastScan) > 0 then
|
|
MTH_CharSavedVariables.trainScan.hasCompletedPetScan = 1
|
|
elseif MTH_CharSavedVariables.trainScan.spellMap then
|
|
for _ in pairs(MTH_CharSavedVariables.trainScan.spellMap) do
|
|
MTH_CharSavedVariables.trainScan.hasCompletedPetScan = 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
MTH_CharSavedVariables.petTraining = MTH_CharSavedVariables.trainScan
|
|
MTH_PT_PromoteUnrankedKnownAliases(MTH_CharSavedVariables.trainScan)
|
|
return MTH_CharSavedVariables.trainScan
|
|
end
|
|
|
|
function MTH_GetPetTrainingSnapshot()
|
|
return MTH_PT_GetStore()
|
|
end
|
|
|
|
function MTH_GetKnownPetSpellMap()
|
|
local store = MTH_PT_GetStore()
|
|
if type(store) ~= "table" then
|
|
return nil
|
|
end
|
|
return store.spellMap
|
|
end
|
|
|
|
local function MTH_PT_ParseRank(name, subText)
|
|
local rankNumber = nil
|
|
local sub = tostring(subText or "")
|
|
if sub ~= "" then
|
|
local _, _, subRank = string.find(sub, "(%d+)")
|
|
rankNumber = tonumber(subRank)
|
|
end
|
|
if not rankNumber then
|
|
local nm = tostring(name or "")
|
|
local _, _, nameRank = string.find(nm, "(%d+)")
|
|
rankNumber = tonumber(nameRank)
|
|
end
|
|
if not rankNumber then
|
|
local full = tostring(name or "") .. " " .. tostring(subText or "")
|
|
local _, _, fullRank = string.find(full, "(%d+)")
|
|
rankNumber = tonumber(fullRank)
|
|
end
|
|
if rankNumber then
|
|
return "Rank " .. tostring(rankNumber), rankNumber
|
|
end
|
|
return "", nil
|
|
end
|
|
|
|
local function MTH_PT_NormalizeName(name)
|
|
local label = tostring(name or "")
|
|
label = string.gsub(label, "^%s+", "")
|
|
label = string.gsub(label, "%s+$", "")
|
|
return label
|
|
end
|
|
|
|
local function MTH_PT_BaseAbility(name)
|
|
local base = MTH_PT_NormalizeName(name)
|
|
base = string.gsub(base, "%s*%([^%)]*%d+[^%)]*%)", "")
|
|
base = string.gsub(base, "%s*%([Rr]ank[^%)]*%)", "")
|
|
base = string.gsub(base, "%s+[Rr]ank%s+%d+", "")
|
|
base = string.gsub(base, "%s+%d+$", "")
|
|
base = string.gsub(base, "%s+$", "")
|
|
return base
|
|
end
|
|
|
|
local function MTH_PT_MakeToken(name, subText)
|
|
local base = MTH_PT_BaseAbility(name)
|
|
local rankText, rankNumber = MTH_PT_ParseRank(name, subText)
|
|
if rankNumber and rankNumber > 0 and not MTH_PT_AbilityHasPositiveRanks(base) then
|
|
rankText = ""
|
|
rankNumber = nil
|
|
end
|
|
if rankText ~= "" then
|
|
return MTH_PT_SafeLower(base) .. "#" .. tostring(rankNumber), base, rankText, rankNumber
|
|
end
|
|
return MTH_PT_SafeLower(base), base, "", nil
|
|
end
|
|
|
|
local function MTH_PT_HasAnyLearnedSpells()
|
|
local store = MTH_PT_GetStore()
|
|
local spellMap = store.spellMap
|
|
if not spellMap then return false end
|
|
for _ in pairs(spellMap) do
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PT_IsBeastTrainingContextActive()
|
|
local frameNames = { "BeastTrainingFrame", "PetTrainingFrame", "CraftFrame" }
|
|
for i = 1, table.getn(frameNames) do
|
|
local frameName = frameNames[i]
|
|
local frame = (type(getglobal) == "function" and getglobal(frameName)) or (_G and _G[frameName])
|
|
if frame then
|
|
if frame.IsVisible and frame:IsVisible() then
|
|
return true
|
|
end
|
|
if frame.IsShown and frame:IsShown() then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PT_GetBeastTrainingRowCount()
|
|
if not MTH_PT_IsBeastTrainingContextActive() then
|
|
return 0
|
|
end
|
|
|
|
local getNumCrafts = MTH_PT_GetGlobal("GetNumCrafts")
|
|
if type(getNumCrafts) ~= "function" then
|
|
return 0
|
|
end
|
|
return tonumber(getNumCrafts()) or 0
|
|
end
|
|
|
|
function MTH_PT_HasBeastTrainingRows()
|
|
return MTH_PT_GetBeastTrainingRowCount() > 0
|
|
end
|
|
|
|
local function MTH_PT_DebugLog(line)
|
|
return
|
|
end
|
|
|
|
local function MTH_PT_IsScanMessageEnabled()
|
|
if not (MTH and MTH.IsMessageEnabled) then
|
|
return true
|
|
end
|
|
return MTH:IsMessageEnabled("beastTrainingScan", true)
|
|
end
|
|
|
|
local function MTH_PT_MakeTokenFromAbilityRank(abilityName, rankNumber)
|
|
local ability = MTH_PT_NormalizeName(abilityName)
|
|
local key = MTH_PT_SafeLower(ability)
|
|
if key == "" then return "", ability, "", nil end
|
|
local rank = tonumber(rankNumber)
|
|
if rank and rank > 0 and not MTH_PT_AbilityHasPositiveRanks(ability) then
|
|
rank = nil
|
|
end
|
|
if rank and rank > 0 then
|
|
return key .. "#" .. tostring(rank), ability, "Rank " .. tostring(rank), rank
|
|
end
|
|
return key, ability, "", nil
|
|
end
|
|
|
|
local function MTH_PT_AbilityExistsInData(baseAbility)
|
|
local ability = MTH_PT_NormalizeName(baseAbility)
|
|
if ability == "" then return false end
|
|
if not MTH_DS_PetSpells or not MTH_DS_PetSpells.byAbility then
|
|
return false
|
|
end
|
|
if MTH_DS_PetSpells.byAbility[ability] then
|
|
return true
|
|
end
|
|
for abilityName in pairs(MTH_DS_PetSpells.byAbility) do
|
|
if MTH_PT_SafeLower(abilityName) == MTH_PT_SafeLower(ability) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
MTH_PT_AbilityHasPositiveRanks = function(baseAbility)
|
|
local ability = MTH_PT_NormalizeName(baseAbility)
|
|
if ability == "" then return false end
|
|
if not MTH_DS_PetSpells or not MTH_DS_PetSpells.byAbility then
|
|
return false
|
|
end
|
|
|
|
local bucket = MTH_DS_PetSpells.byAbility[ability]
|
|
if not bucket then
|
|
for abilityName, row in pairs(MTH_DS_PetSpells.byAbility) do
|
|
if MTH_PT_SafeLower(abilityName) == MTH_PT_SafeLower(ability) then
|
|
bucket = row
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not (bucket and bucket.spells) then
|
|
return false
|
|
end
|
|
|
|
for i = 1, table.getn(bucket.spells) do
|
|
local spell = bucket.spells[i]
|
|
if spell then
|
|
local rank = tonumber(spell.rankNumber)
|
|
if not rank and spell.rank then
|
|
local _, _, parsed = string.find(tostring(spell.rank), "(%d+)")
|
|
rank = tonumber(parsed)
|
|
end
|
|
if rank and rank > 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
MTH_PT_PromoteUnrankedKnownAliases = function(store)
|
|
if not store or not store.hunterKnownMap or not store.spellMap then
|
|
return
|
|
end
|
|
|
|
for token, isKnown in pairs(store.hunterKnownMap) do
|
|
if isKnown == true then
|
|
local _, _, baseToken = string.find(tostring(token), "^(.-)#%d+$")
|
|
if baseToken and baseToken ~= "" then
|
|
local baseRow = store.spellMap[baseToken]
|
|
if baseRow and ((tonumber(baseRow.rankNumber) or 0) <= 0) then
|
|
store.hunterKnownMap[baseToken] = true
|
|
if baseRow.isKnown ~= true then
|
|
baseRow.isKnown = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function MTH_PT_MarkHunterKnownToken(token)
|
|
if token == nil or token == "" then
|
|
return false
|
|
end
|
|
|
|
local store = MTH_PT_GetStore()
|
|
local markToken = token
|
|
local _, _, baseToken = string.find(tostring(token), "^(.-)#%d+$")
|
|
if baseToken and baseToken ~= "" then
|
|
local baseRow = store.spellMap and store.spellMap[baseToken]
|
|
if baseRow and ((tonumber(baseRow.rankNumber) or 0) <= 0) then
|
|
markToken = baseToken
|
|
end
|
|
end
|
|
local changed = false
|
|
if store.hunterKnownMap[markToken] ~= true then
|
|
store.hunterKnownMap[markToken] = true
|
|
changed = true
|
|
end
|
|
if markToken ~= token and store.hunterKnownMap[token] ~= true then
|
|
store.hunterKnownMap[token] = true
|
|
changed = true
|
|
end
|
|
|
|
if store.spellMap and store.spellMap[markToken] and store.spellMap[markToken].isKnown ~= true then
|
|
store.spellMap[markToken].isKnown = true
|
|
changed = true
|
|
end
|
|
if markToken ~= token and store.spellMap and store.spellMap[token] and store.spellMap[token].isKnown ~= true then
|
|
store.spellMap[token].isKnown = true
|
|
changed = true
|
|
end
|
|
|
|
if changed and MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("hunter-known token promoted: " .. tostring(token))
|
|
end
|
|
|
|
return changed
|
|
end
|
|
|
|
local function MTH_PT_IsHunterTokenKnown(token)
|
|
if token == nil or token == "" then
|
|
return false
|
|
end
|
|
|
|
local store = MTH_PT_GetStore()
|
|
if store.hunterKnownMap and store.hunterKnownMap[token] == true then
|
|
return true
|
|
end
|
|
|
|
if store.spellMap and store.spellMap[token] and store.spellMap[token].isKnown == true then
|
|
return true
|
|
end
|
|
|
|
local _, _, baseToken = string.find(tostring(token), "^(.-)#%d+$")
|
|
if baseToken and baseToken ~= "" then
|
|
if store.hunterKnownMap and store.hunterKnownMap[baseToken] == true then
|
|
return true
|
|
end
|
|
if store.spellMap and store.spellMap[baseToken] and store.spellMap[baseToken].isKnown == true then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Rank-strict variant of MTH_PT_IsHunterTokenKnown: only the EXACT token counts, with
|
|
-- no fall back to the base ability. Knowing Charge (Rank 1) must NOT make Charge (Rank 2)
|
|
-- look already-learned, otherwise learning a new rank is never recorded or announced.
|
|
local function MTH_PT_IsExactTokenKnown(token)
|
|
if token == nil or token == "" then
|
|
return false
|
|
end
|
|
|
|
local store = MTH_PT_GetStore()
|
|
if store.hunterKnownMap and store.hunterKnownMap[token] == true then
|
|
return true
|
|
end
|
|
if store.spellMap and store.spellMap[token] and store.spellMap[token].isKnown == true then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Authoritative, rank-strict check for whether the HUNTER has learned a pet ability at a
|
|
-- given rank. Consults both hunterKnownMap and spellMap, so callers (e.g. the tooltip hint)
|
|
-- stay correct even when a full Beast Training rescan has not yet rebuilt spellMap rows.
|
|
function MTH_IsPetAbilityKnownByHunter(abilityName, rankNumber)
|
|
local ability = MTH_PT_NormalizeName(abilityName)
|
|
if ability == "" then
|
|
return false
|
|
end
|
|
local token = MTH_PT_MakeTokenFromAbilityRank(ability, rankNumber)
|
|
if token == "" then
|
|
return false
|
|
end
|
|
return MTH_PT_IsExactTokenKnown(token)
|
|
end
|
|
|
|
local function MTH_PT_ProcessLearnSystemMessage(rawMessage)
|
|
local message = tostring(rawMessage or "")
|
|
if message == "" then return false 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+$", "")
|
|
|
|
local _, _, abilityName, rankText = string.find(message, "^[Yy]ou have learned a new spell:%s*(.-)%s*%([Rr]ank%s*(%d+)%)%.?$")
|
|
if not abilityName or not rankText then
|
|
_, _, abilityName, rankText = string.find(message, "^[Yy]ou have learned a new spell:%s*(.-)%s*[Rr]ank%s*(%d+)%.?$")
|
|
end
|
|
if not abilityName or not rankText then
|
|
return false
|
|
end
|
|
|
|
local rankNumber = tonumber(rankText)
|
|
if not rankNumber then
|
|
return false
|
|
end
|
|
|
|
abilityName = MTH_PT_NormalizeName(abilityName)
|
|
if abilityName == "" then return false end
|
|
if not MTH_PT_AbilityExistsInData(abilityName) then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("learn-msg ignored (not pet ability dataset): '" .. tostring(abilityName) .. "'")
|
|
end
|
|
return false
|
|
end
|
|
|
|
local token = MTH_PT_MakeTokenFromAbilityRank(abilityName, rankNumber)
|
|
if token == "" then return false end
|
|
|
|
local alreadyKnown = MTH_PT_IsExactTokenKnown(token)
|
|
local changed = false
|
|
if not alreadyKnown then
|
|
changed = MTH_PT_MarkHunterKnownToken(token)
|
|
end
|
|
|
|
local petRecorded = false
|
|
if type(MTH_PETS_RecordCurrentPetLearnedAbility) == "function" then
|
|
petRecorded = MTH_PETS_RecordCurrentPetLearnedAbility(abilityName, rankNumber, "beasttraining:learn-msg") and true or false
|
|
end
|
|
|
|
if changed or petRecorded then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("learn-msg captured: source='you have learned a new spell' ability='" .. tostring(abilityName) .. "' rank=" .. tostring(rankNumber) .. " token='" .. tostring(token) .. "' changed=" .. tostring(changed) .. " petRecorded=" .. tostring(petRecorded))
|
|
end
|
|
local label = tostring(abilityName)
|
|
if rankNumber and rankNumber > 0 then
|
|
label = label .. " (Rank " .. tostring(rankNumber) .. ")"
|
|
end
|
|
if MTH and MTH.Print then
|
|
if MTH:IsMessageEnabled("beastTrainingScan", true) then
|
|
if changed then
|
|
MTH:Print("Pet ability learned and recorded: " .. label .. ". It is now marked as known in MetaHunt.")
|
|
else
|
|
MTH:Print("Pet ability learned for current pet: " .. label .. ".")
|
|
end
|
|
end
|
|
end
|
|
if changed then
|
|
MTH_PT_ApplyScan("learn-msg")
|
|
end
|
|
if type(MTH_PSP_RequestScan) == "function" then
|
|
MTH_PSP_RequestScan("train-learn-msg")
|
|
end
|
|
elseif alreadyKnown and MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("learn-msg ignored (already known and no active pet row update): token='" .. tostring(token) .. "'")
|
|
end
|
|
|
|
return changed or petRecorded
|
|
end
|
|
|
|
local function MTH_PT_CollectKnownBeastTrainingMap()
|
|
if not MTH_PT_IsBeastTrainingContextActive() then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("collect beast-training known skipped: explicit beast training context not active")
|
|
end
|
|
return {}
|
|
end
|
|
|
|
local getNumCrafts = MTH_PT_GetGlobal("GetNumCrafts")
|
|
local getCraftInfo = MTH_PT_GetGlobal("GetCraftInfo")
|
|
if type(getNumCrafts) ~= "function" or type(getCraftInfo) ~= "function" then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("collect beast-training known unavailable: GetNumCrafts=" .. tostring(type(getNumCrafts)) .. ", GetCraftInfo=" .. tostring(type(getCraftInfo)))
|
|
end
|
|
return {}
|
|
end
|
|
|
|
local totalCrafts = tonumber(getNumCrafts()) or 0
|
|
local knownMap = {}
|
|
if totalCrafts <= 0 then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("collect beast-training known empty: totalRows=0")
|
|
end
|
|
return knownMap
|
|
end
|
|
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("collect beast-training known start: totalRows=" .. tostring(totalCrafts))
|
|
end
|
|
for i = 1, totalCrafts do
|
|
local craftName, craftSubSpellName, craftType = getCraftInfo(i)
|
|
local typeText = tostring(craftType or "")
|
|
local token, base, parsedRankText, rankNumber = MTH_PT_MakeToken(craftName, craftSubSpellName)
|
|
|
|
if token ~= "" then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("beast-training row=" .. tostring(i) .. " token='" .. tostring(token) .. "' name='" .. tostring(base or "") .. "' rank='" .. tostring(parsedRankText or "") .. "' rankNumber=" .. tostring(rankNumber) .. " type='" .. typeText .. "'")
|
|
end
|
|
knownMap[token] = {
|
|
name = MTH_PT_NormalizeName(base),
|
|
rank = parsedRankText,
|
|
rankNumber = rankNumber,
|
|
fullName = MTH_PT_NormalizeName(craftName),
|
|
subText = tostring(craftSubSpellName or ""),
|
|
isKnown = true,
|
|
scannedAt = time(),
|
|
rowType = typeText,
|
|
}
|
|
end
|
|
end
|
|
if MTH_PT_DebugLog then
|
|
local total = 0
|
|
for _ in pairs(knownMap) do total = total + 1 end
|
|
MTH_PT_DebugLog("collect beast-training known end: tokens=" .. tostring(total))
|
|
end
|
|
return knownMap
|
|
end
|
|
|
|
function MTH_PT_ApplyScan(triggerReason)
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("scan start: trigger=" .. tostring(triggerReason or ""))
|
|
end
|
|
|
|
local store = MTH_PT_GetStore()
|
|
local oldMap = store.spellMap
|
|
if not oldMap then oldMap = {} end
|
|
local hunterKnownMap = store.hunterKnownMap or {}
|
|
|
|
local knownMap = MTH_PT_CollectKnownBeastTrainingMap()
|
|
local knownCount = 0
|
|
for _ in pairs(knownMap) do knownCount = knownCount + 1 end
|
|
if knownCount <= 0 then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("scan abort: beast-training known count=0")
|
|
end
|
|
return false, 0, 0, 0
|
|
end
|
|
for token in pairs(knownMap) do
|
|
hunterKnownMap[token] = true
|
|
end
|
|
for token, row in pairs(oldMap) do
|
|
if row and row.isKnown then
|
|
hunterKnownMap[token] = true
|
|
end
|
|
end
|
|
for token in pairs(knownMap) do
|
|
if hunterKnownMap[token] == nil then
|
|
hunterKnownMap[token] = false
|
|
end
|
|
end
|
|
store.hunterKnownMap = hunterKnownMap
|
|
|
|
local newMap = {}
|
|
if MTH_DS_PetSpells and MTH_DS_PetSpells.byAbility then
|
|
for abilityName, bucket in pairs(MTH_DS_PetSpells.byAbility) do
|
|
if bucket and bucket.spells then
|
|
for i = 1, table.getn(bucket.spells) do
|
|
local spell = bucket.spells[i]
|
|
if spell then
|
|
local rankNumber = tonumber(spell.rankNumber)
|
|
if not rankNumber and spell.rank then
|
|
local _, _, parsed = string.find(tostring(spell.rank), "(%d+)")
|
|
rankNumber = tonumber(parsed)
|
|
end
|
|
local token, baseName, rankText, parsedRankNumber = MTH_PT_MakeTokenFromAbilityRank(abilityName, rankNumber)
|
|
if token ~= "" then
|
|
local tpCost = tonumber(spell.trainingPointCost)
|
|
if not tpCost and spell.cost then
|
|
local _, _, parsedCost = string.find(tostring(spell.cost), "(%d+)")
|
|
tpCost = tonumber(parsedCost)
|
|
end
|
|
newMap[token] = {
|
|
name = baseName,
|
|
rank = rankText,
|
|
rankNumber = parsedRankNumber,
|
|
fullName = rankText ~= "" and (baseName .. " " .. rankText) or baseName,
|
|
subText = rankText,
|
|
serviceType = "datastore",
|
|
reqLevel = tonumber(spell.trainLevel),
|
|
isKnown = hunterKnownMap[token] and true or false,
|
|
isKnownPet = knownMap[token] and true or false,
|
|
scannedAt = time(),
|
|
trainingPointCost = tpCost,
|
|
learnMethod = spell.learnMethod,
|
|
icon = spell.icon,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for token, row in pairs(knownMap) do
|
|
if newMap[token] then
|
|
newMap[token].isKnownPet = true
|
|
newMap[token].scannedAt = time()
|
|
else
|
|
newMap[token] = {
|
|
name = row.name,
|
|
rank = row.rank,
|
|
rankNumber = row.rankNumber,
|
|
fullName = row.fullName,
|
|
subText = row.subText,
|
|
serviceType = "beast-training",
|
|
reqLevel = nil,
|
|
isKnown = hunterKnownMap[token] and true or false,
|
|
isKnownPet = true,
|
|
scannedAt = time(),
|
|
trainingPointCost = nil,
|
|
}
|
|
end
|
|
end
|
|
|
|
local discovered = 0
|
|
for _ in pairs(newMap) do discovered = discovered + 1 end
|
|
if discovered <= 0 then
|
|
return false, 0, 0, 0
|
|
end
|
|
|
|
local added = 0
|
|
local removed = 0
|
|
local addedDetails = {}
|
|
for token in pairs(newMap) do
|
|
if not oldMap[token] then
|
|
added = added + 1
|
|
local row = newMap[token]
|
|
local spellName = MTH_PT_NormalizeName((row and row.name) or "")
|
|
local rankText = tostring((row and row.rank) or "")
|
|
if spellName == "" then
|
|
spellName = MTH_PT_NormalizeName((row and row.fullName) or "")
|
|
end
|
|
if spellName ~= "" then
|
|
local label = spellName
|
|
if rankText ~= "" then
|
|
label = label .. " " .. rankText
|
|
end
|
|
table.insert(addedDetails, label)
|
|
end
|
|
end
|
|
end
|
|
for token in pairs(oldMap) do
|
|
if not newMap[token] then
|
|
removed = removed + 1
|
|
end
|
|
end
|
|
|
|
store.spellMap = newMap
|
|
store.lastScan = time()
|
|
store.hasCompletedPetScan = 1
|
|
MTH_CharSavedVariables.trainScan = store
|
|
MTH_CharSavedVariables.petTraining = store
|
|
|
|
local totalRanks = 0
|
|
local baseSet = {}
|
|
for _, row in pairs(newMap) do
|
|
totalRanks = totalRanks + 1
|
|
local base = MTH_PT_SafeLower(row and row.name)
|
|
if base ~= "" then baseSet[base] = true end
|
|
end
|
|
local totalBaselines = 0
|
|
for _ in pairs(baseSet) do totalBaselines = totalBaselines + 1 end
|
|
|
|
if triggerReason == "initial" then
|
|
if MTH_PT_IsScanMessageEnabled() then
|
|
MTH:Print("Initial train scan complete: " .. tostring(totalBaselines) .. " baselines / " .. tostring(totalRanks) .. " ranks recorded for this character.")
|
|
end
|
|
elseif added > 0 then
|
|
table.sort(addedDetails)
|
|
local detailsText = table.concat(addedDetails, ", ")
|
|
if detailsText ~= "" then
|
|
local msg = "New pet data recorded (" .. tostring(added) .. " new rank(s)): " .. detailsText .. ". Known totals for this character: " .. tostring(totalBaselines) .. " ability type(s), " .. tostring(totalRanks) .. " rank(s)."
|
|
if MTH_PT_IsScanMessageEnabled() then
|
|
MTH:Print(msg)
|
|
end
|
|
else
|
|
local msg = "New pet data recorded (" .. tostring(added) .. " new rank(s)). Known totals for this character: " .. tostring(totalBaselines) .. " ability type(s), " .. tostring(totalRanks) .. " rank(s)."
|
|
if MTH_PT_IsScanMessageEnabled() then
|
|
MTH:Print(msg)
|
|
end
|
|
end
|
|
end
|
|
|
|
return true, added, totalBaselines, totalRanks
|
|
end
|
|
|
|
local function MTH_PT_ShouldScanNow()
|
|
return MTH_PT_HasBeastTrainingRows()
|
|
end
|
|
|
|
MTH_PT_IsPetBookVisible = function()
|
|
local frameNames = { "BeastTrainingFrame", "PetTrainingFrame", "CraftFrame" }
|
|
for i = 1, table.getn(frameNames) do
|
|
local frameName = frameNames[i]
|
|
local frame = (type(getglobal) == "function" and getglobal(frameName)) or (_G and _G[frameName])
|
|
if frame then
|
|
if frame.IsVisible and frame:IsVisible() then
|
|
return true
|
|
end
|
|
if frame.IsShown and frame:IsShown() then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MTH_PT_RunDoubleScan(triggerBase)
|
|
local triggerText = tostring(triggerBase or "")
|
|
local feedbackOnOpen = (
|
|
triggerText == "petbook-open"
|
|
or triggerText == "craft-open"
|
|
or triggerText == "craft-update"
|
|
or triggerText == "beast-training-rows-open"
|
|
or triggerText == "petbook-visible-poll"
|
|
)
|
|
if not MTH_PT_ShouldScanNow() then
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("scan skipped: no beast-training rows, trigger=" .. tostring(triggerBase or ""))
|
|
end
|
|
if feedbackOnOpen and MTH and MTH.Print and MTH_PT_IsScanMessageEnabled() then
|
|
MTH:Print("Beast Training opened, but scan did not run yet (no scannable rows detected).")
|
|
end
|
|
return false
|
|
end
|
|
|
|
local t0 = time()
|
|
local ok, added, baselines, ranks = MTH_PT_ApplyScan(triggerText .. ":single")
|
|
local t1 = time()
|
|
|
|
if MTH_PT_DebugLog then
|
|
MTH_PT_DebugLog("scan complete: trigger=" .. tostring(triggerBase or "") .. " ok=" .. tostring(ok and true or false) .. " dt=" .. tostring((t1 or 0) - (t0 or 0)) .. "s")
|
|
end
|
|
if feedbackOnOpen and MTH and MTH.Print and MTH_PT_IsScanMessageEnabled() then
|
|
if ok then
|
|
local addedCount = tonumber(added) or 0
|
|
local msg = "Beast Training scan complete: " .. tostring(baselines or 0) .. " baselines / " .. tostring(ranks or 0) .. " ranks."
|
|
if addedCount > 0 then
|
|
msg = msg .. " Added +" .. tostring(addedCount) .. " new rank(s)."
|
|
end
|
|
MTH:Print(msg)
|
|
else
|
|
MTH:Print("Beast Training scan attempted, but no data was recorded.")
|
|
end
|
|
end
|
|
|
|
return ok and true or false
|
|
end
|
|
|
|
local MTH_PetTrainingFrame = nil
|
|
local MTH_PT_PetBookWasVisible = false
|
|
local MTH_PT_PetBookPollElapsed = 0
|
|
local MTH_PT_TrainingRowsWerePresent = false
|
|
local MTH_PT_LastTrainingRowCount = 0
|
|
local MTH_PT_LastAutoDoubleScanAt = 0
|
|
|
|
local function MTH_PT_RunDoubleScanThrottled(triggerBase, minIntervalSeconds)
|
|
local now = tonumber(time()) or 0
|
|
local minInterval = tonumber(minIntervalSeconds) or 0
|
|
if minInterval > 0 and (now - (MTH_PT_LastAutoDoubleScanAt or 0)) < minInterval then
|
|
return false
|
|
end
|
|
MTH_PT_LastAutoDoubleScanAt = now
|
|
return MTH_PT_RunDoubleScan(triggerBase)
|
|
end
|
|
|
|
local function MTH_PT_OnEvent(_, evt, eventArg1)
|
|
evt = evt or event
|
|
eventArg1 = eventArg1 or arg1
|
|
if not evt then
|
|
return
|
|
end
|
|
|
|
if evt == "PLAYER_ENTERING_WORLD" then
|
|
local playerLevel = 0
|
|
if type(UnitLevel) == "function" then
|
|
playerLevel = tonumber(UnitLevel("player")) or 0
|
|
end
|
|
if UnitClass then
|
|
local _, classToken = UnitClass("player")
|
|
if classToken ~= "HUNTER" then return end
|
|
end
|
|
if playerLevel >= 12 and not MTH_PT_HasAnyLearnedSpells() then
|
|
local store = MTH_PT_GetStore()
|
|
local now = time()
|
|
if not store.lastPrompt or (now - store.lastPrompt) > 60 then
|
|
store.lastPrompt = now
|
|
if MTH_PT_IsScanMessageEnabled() then
|
|
MTH:Print("No Beast Training scan found for this character. Open Beast Training (right panel) to record pet spell ranks.")
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if evt == "LEARNED_SPELL_IN_TAB" then
|
|
if MTH_PT_ShouldScanNow() then
|
|
MTH_PT_RunDoubleScan("petbook-visible:" .. tostring(evt))
|
|
end
|
|
return
|
|
end
|
|
|
|
if evt == "PET_TRAINING_SHOW" then
|
|
MTH_PT_RunDoubleScan("petbook-open")
|
|
return
|
|
end
|
|
|
|
if evt == "CRAFT_SHOW" then
|
|
MTH_PT_RunDoubleScanThrottled("craft-open", 1)
|
|
return
|
|
end
|
|
|
|
if evt == "CRAFT_UPDATE" then
|
|
if MTH_PT_ShouldScanNow() then
|
|
MTH_PT_RunDoubleScanThrottled("craft-update", 1)
|
|
end
|
|
return
|
|
end
|
|
|
|
if evt == "SPELLS_CHANGED" then
|
|
if MTH_PT_ShouldScanNow() then
|
|
MTH_PT_RunDoubleScan("petbook-visible:" .. tostring(evt))
|
|
end
|
|
return
|
|
end
|
|
|
|
if evt == "UNIT_PET" or evt == "PET_BAR_UPDATE" then
|
|
if evt == "UNIT_PET" then
|
|
local unit = eventArg1
|
|
if unit ~= "player" then return end
|
|
end
|
|
if MTH_PT_ShouldScanNow() then
|
|
MTH_PT_RunDoubleScan("petbook-visible:" .. tostring(evt))
|
|
end
|
|
return
|
|
end
|
|
|
|
if evt == "CHAT_MSG_SYSTEM" then
|
|
MTH_PT_ProcessLearnSystemMessage(eventArg1)
|
|
return
|
|
end
|
|
end
|
|
|
|
function MTH_PT_InitService()
|
|
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate("trainscan-init") then
|
|
return
|
|
end
|
|
|
|
if MTH_PetTrainingFrame then
|
|
return
|
|
end
|
|
MTH_PetTrainingFrame = CreateFrame("Frame", "MTH_PetTrainingScan")
|
|
MTH_PetTrainingFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
MTH_PetTrainingFrame:RegisterEvent("LEARNED_SPELL_IN_TAB")
|
|
MTH_PetTrainingFrame:RegisterEvent("PET_TRAINING_SHOW")
|
|
MTH_PetTrainingFrame:RegisterEvent("SPELLS_CHANGED")
|
|
MTH_PetTrainingFrame:RegisterEvent("UNIT_PET")
|
|
MTH_PetTrainingFrame:RegisterEvent("PET_BAR_UPDATE")
|
|
MTH_PetTrainingFrame:RegisterEvent("CHAT_MSG_SYSTEM")
|
|
MTH_PetTrainingFrame:RegisterEvent("CRAFT_SHOW")
|
|
MTH_PetTrainingFrame:RegisterEvent("CRAFT_UPDATE")
|
|
MTH_PetTrainingFrame:SetScript("OnEvent", MTH_PT_OnEvent)
|
|
MTH_PetTrainingFrame:SetScript("OnUpdate", function()
|
|
MTH_PT_PetBookPollElapsed = (MTH_PT_PetBookPollElapsed or 0) + (arg1 or 0)
|
|
if MTH_PT_PetBookPollElapsed < 0.20 then
|
|
return
|
|
end
|
|
MTH_PT_PetBookPollElapsed = 0
|
|
|
|
local rowCount = MTH_PT_GetBeastTrainingRowCount()
|
|
local hasRows = rowCount > 0
|
|
if hasRows and not MTH_PT_TrainingRowsWerePresent then
|
|
MTH_PT_RunDoubleScanThrottled("beast-training-rows-open", 1)
|
|
end
|
|
|
|
if hasRows and rowCount ~= (MTH_PT_LastTrainingRowCount or 0) then
|
|
MTH_PT_RunDoubleScanThrottled("beast-training-rows-changed", 1)
|
|
end
|
|
|
|
if hasRows then
|
|
MTH_PT_RunDoubleScanThrottled("beast-training-rows-present", 8)
|
|
end
|
|
|
|
MTH_PT_TrainingRowsWerePresent = hasRows
|
|
MTH_PT_LastTrainingRowCount = rowCount
|
|
|
|
local visible = MTH_PT_IsPetBookVisible()
|
|
if visible and not MTH_PT_PetBookWasVisible then
|
|
MTH_PT_RunDoubleScanThrottled("petbook-visible-poll", 1)
|
|
end
|
|
|
|
MTH_PT_PetBookWasVisible = visible and true or false
|
|
end)
|
|
end
|
|
|
|
function MTH_PT_InitBootstrap()
|
|
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate("trainscan-bootstrap") then
|
|
return
|
|
end
|
|
if MTH_PetTrainingFrame or MTH_PT_BootstrapFrame then
|
|
return
|
|
end
|
|
|
|
local frame = CreateFrame("Frame")
|
|
if not frame then
|
|
return
|
|
end
|
|
frame:RegisterEvent("PET_TRAINING_SHOW")
|
|
frame:RegisterEvent("CRAFT_SHOW")
|
|
frame:SetScript("OnEvent", function()
|
|
if event ~= "PET_TRAINING_SHOW" and event ~= "CRAFT_SHOW" then
|
|
return
|
|
end
|
|
MTH_PT_InitService()
|
|
if type(MTH_PT_OnEvent) == "function" then
|
|
MTH_PT_OnEvent(MTH_PetTrainingFrame, event)
|
|
end
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnEvent", nil)
|
|
MTH_PT_BootstrapFrame = nil
|
|
end)
|
|
MTH_PT_BootstrapFrame = frame
|
|
end
|
|
|
|
function MTH_PT_ShutdownService(_reason)
|
|
if not MTH_PetTrainingFrame then
|
|
return
|
|
end
|
|
|
|
MTH_PetTrainingFrame:UnregisterAllEvents()
|
|
MTH_PetTrainingFrame:SetScript("OnEvent", nil)
|
|
MTH_PetTrainingFrame:SetScript("OnUpdate", nil)
|
|
MTH_PetTrainingFrame = nil
|
|
|
|
MTH_PT_PetBookWasVisible = false
|
|
MTH_PT_PetBookPollElapsed = 0
|
|
MTH_PT_TrainingRowsWerePresent = false
|
|
MTH_PT_LastTrainingRowCount = 0
|
|
MTH_PT_LastAutoDoubleScanAt = 0
|
|
end
|
|
|
|
function MTH_CommandPetScan()
|
|
local function requestBeastTrainingOpen()
|
|
if MTH_PT_IsPetBookVisible and MTH_PT_IsPetBookVisible() then
|
|
return true
|
|
end
|
|
|
|
local bookTypePet = MTH_PT_GetGlobal("BOOKTYPE_PET")
|
|
local openSpellBook = MTH_PT_GetGlobal("OpenSpellBook")
|
|
if type(openSpellBook) == "function" and bookTypePet then
|
|
openSpellBook(bookTypePet)
|
|
end
|
|
|
|
if MTH_PT_IsPetBookVisible and MTH_PT_IsPetBookVisible() then
|
|
return true
|
|
end
|
|
|
|
local toggleSpellBook = MTH_PT_GetGlobal("ToggleSpellBook")
|
|
if type(toggleSpellBook) == "function" then
|
|
if bookTypePet then
|
|
toggleSpellBook(bookTypePet)
|
|
else
|
|
toggleSpellBook()
|
|
end
|
|
end
|
|
|
|
return MTH_PT_IsPetBookVisible and MTH_PT_IsPetBookVisible() or false
|
|
end
|
|
|
|
if UnitClass then
|
|
local _, classToken = UnitClass("player")
|
|
if classToken ~= "HUNTER" then
|
|
MTH:Print("Train scan is hunter-only.")
|
|
return
|
|
end
|
|
end
|
|
|
|
if not MTH_PT_ShouldScanNow() then
|
|
local opened = requestBeastTrainingOpen()
|
|
if not MTH_PT_ShouldScanNow() then
|
|
if opened then
|
|
MTH:Print("Beast Training is open, but no scannable rows were detected yet. Try once more after the list fully loads.")
|
|
else
|
|
MTH:Print("Could not detect Beast Training rows. Open Beast Training (right panel) and run /mth trainscan again.")
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
local ok, added, baselines, ranks = MTH_PT_ApplyScan("manual")
|
|
if not ok then
|
|
MTH:Print("Train scan failed. Keep Beast Training open and try /mth trainscan again.")
|
|
return
|
|
end
|
|
|
|
MTH:Print("Train scan saved for this character: " .. tostring(baselines) .. " baselines / " .. tostring(ranks) .. " ranks.")
|
|
if (tonumber(added) or 0) > 0 then
|
|
MTH:Print("Newly recorded ranks: +" .. tostring(added))
|
|
end
|
|
end
|
|
|
|
function MTH_CommandTrainScan()
|
|
return MTH_CommandPetScan()
|
|
end
|
|
|
|
MTH_TR_InitService = MTH_PT_InitService
|
|
MTH_TR_ShutdownService = MTH_PT_ShutdownService
|
|
MTH_TR_InitBootstrap = MTH_PT_InitBootstrap
|