449 lines
14 KiB
Lua
449 lines
14 KiB
Lua
-- Ade Alts: scan
|
|
local AA = AdeAlts
|
|
|
|
----------------------------------------------------------------------
|
|
-- scanning: trade skill window
|
|
----------------------------------------------------------------------
|
|
local tip = CreateFrame("GameTooltip", "AdeAltsTip", nil, "GameTooltipTemplate")
|
|
|
|
local function TipCooldown(index)
|
|
if GetTradeSkillCooldown then
|
|
local c = GetTradeSkillCooldown(index)
|
|
if c and c > 0 then return c end
|
|
end
|
|
tip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
tip:ClearLines()
|
|
tip:SetTradeSkillItem(index)
|
|
for i = 1, tip:NumLines() do
|
|
local fs = getglobal("AdeAltsTipTextLeft" .. i)
|
|
local t = fs and fs:GetText()
|
|
if t then
|
|
local l = string.lower(t)
|
|
if string.find(l, "cooldown") and string.find(l, "remaining") then
|
|
local d = AA.ParseDuration(t)
|
|
if d then return d end
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- remember the known recipes of a profession (read from its trade skill / craft window)
|
|
local function StoreRecipes(prof, recs, rank, maxrank)
|
|
if not AA.me or not prof or prof == "" or table.getn(recs) == 0 then return end
|
|
AA.me.prof = AA.me.prof or {}
|
|
local p = AA.me.prof[prof]
|
|
if not p then
|
|
p = {}
|
|
AA.me.prof[prof] = p
|
|
end
|
|
if rank and rank > 0 and (p.r ~= rank or p.m ~= maxrank) then
|
|
p.r = rank
|
|
p.m = maxrank
|
|
p.ts = time()
|
|
AA.fileDirty = true
|
|
end
|
|
table.sort(recs)
|
|
local sig = table.concat(recs, "~")
|
|
if sig ~= p.sig then
|
|
p.rec = recs
|
|
p.sig = sig
|
|
p.rts = time()
|
|
AA.fileDirty = true
|
|
end
|
|
end
|
|
|
|
function AA.ScanTradeSkills()
|
|
if AA.scanning or not AA.me then return end
|
|
if GetTime() - AA.lastScan < 1 then return end
|
|
local n = GetNumTradeSkills()
|
|
if not n or n == 0 then return end
|
|
AA.scanning = true
|
|
|
|
-- expand collapsed headers temporarily so every recipe is visible
|
|
local expanded = {}
|
|
for i = n, 1, -1 do
|
|
local name, ttype, _, isExp = GetTradeSkillInfo(i)
|
|
if ttype == "header" and not isExp then
|
|
expanded[name] = true
|
|
ExpandTradeSkillSubClass(i)
|
|
end
|
|
end
|
|
|
|
n = GetNumTradeSkills()
|
|
local nowt = time()
|
|
local recs = {}
|
|
for i = 1, n do
|
|
local name, ttype = GetTradeSkillInfo(i)
|
|
if name and ttype ~= "header" then table.insert(recs, name) end
|
|
if name and ttype ~= "header" and AA.IsTracked(name) then
|
|
local rem = TipCooldown(i)
|
|
if rem then
|
|
AA.db.tipWorks = true
|
|
AA.SetCD(name, nowt + rem)
|
|
else
|
|
local cur = AA.me.cds[name]
|
|
if AA.prov[name] and nowt - AA.prov[name] < 10 then
|
|
-- just crafted, keep provisional value
|
|
elseif cur and cur > nowt and not AA.db.tipWorks then
|
|
-- tooltips don't expose cooldowns on this client: keep the estimate
|
|
else
|
|
AA.SetCD(name, 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local pn, pr, pm = GetTradeSkillLine()
|
|
StoreRecipes(pn, recs, pr, pm)
|
|
|
|
-- restore collapsed state
|
|
for i = GetNumTradeSkills(), 1, -1 do
|
|
local name, ttype, _, isExp = GetTradeSkillInfo(i)
|
|
if ttype == "header" and expanded[name] and isExp then
|
|
CollapseTradeSkillSubClass(i)
|
|
end
|
|
end
|
|
|
|
AA.scanning = false
|
|
AA.lastScan = GetTime()
|
|
if AA.Refresh then AA.Refresh() end
|
|
end
|
|
|
|
-- craft window (Enchanting uses it instead of the trade skill window)
|
|
local scanningCraft, lastCraftScan = false, 0
|
|
function AA.ScanCrafts()
|
|
if scanningCraft or not AA.me or not GetNumCrafts then return end
|
|
if GetTime() - lastCraftScan < 1 then return end
|
|
local n = GetNumCrafts()
|
|
if not n or n == 0 then return end
|
|
local pn, pr, pm
|
|
if GetCraftDisplaySkillLine then pn, pr, pm = GetCraftDisplaySkillLine() end
|
|
if not pn or pn == "" then return end -- e.g. hunter pet training
|
|
scanningCraft = true
|
|
if ExpandCraftSkillLine then
|
|
for i = n, 1, -1 do
|
|
local name, sub, ctype, _, isExp = GetCraftInfo(i)
|
|
if ctype == "header" and not isExp then ExpandCraftSkillLine(i) end
|
|
end
|
|
end
|
|
n = GetNumCrafts()
|
|
local recs = {}
|
|
for i = 1, n do
|
|
local name, sub, ctype = GetCraftInfo(i)
|
|
if name and ctype ~= "header" then table.insert(recs, name) end
|
|
end
|
|
StoreRecipes(pn, recs, pr, pm)
|
|
scanningCraft = false
|
|
lastCraftScan = GetTime()
|
|
if AA.Refresh then AA.Refresh() end
|
|
end
|
|
|
|
-- crafting hook: provisional cooldown the moment a tracked recipe is started
|
|
local origDoTradeSkill = DoTradeSkill
|
|
DoTradeSkill = function(index, count)
|
|
if AA.me and index then
|
|
local name, ttype, avail = GetTradeSkillInfo(index)
|
|
if name and avail and avail > 0 and AA.IsTracked(name) then
|
|
local d = AA.DEFAULTS[name]
|
|
if d then
|
|
AA.SetCD(name, time() + d)
|
|
AA.prov[name] = time()
|
|
end
|
|
AA.After(5, function() if TradeSkillFrame and TradeSkillFrame:IsVisible() then AA.ScanTradeSkills() end end)
|
|
AA.After(12, function() if TradeSkillFrame and TradeSkillFrame:IsVisible() then AA.ScanTradeSkills() end end)
|
|
end
|
|
end
|
|
return origDoTradeSkill(index, count)
|
|
end
|
|
|
|
----------------------------------------------------------------------
|
|
-- scanning: bags (Salt Shaker etc.)
|
|
----------------------------------------------------------------------
|
|
local BYNAME = {}
|
|
for id, it in pairs(AA.ITEMS) do BYNAME[it.key] = it end
|
|
|
|
-- match a tracked item by id, or by name as a fallback (in case the item id differs on this server)
|
|
function AA.ItemFromLink(link)
|
|
if not link then return nil end
|
|
local _, _, id = string.find(link, "item:(%d+)")
|
|
local it = id and AA.ITEMS[tonumber(id)]
|
|
if not it then
|
|
local _, _, nm = string.find(link, "%[(.-)%]")
|
|
if nm then it = BYNAME[nm] end
|
|
end
|
|
return it
|
|
end
|
|
|
|
AA.bankOpen = false
|
|
|
|
local function FindItemDef(link)
|
|
local _, _, id = string.find(link, "item:(%d+)")
|
|
local it = id and AA.ITEMS[tonumber(id)]
|
|
if it then return it end
|
|
local _, _, nm = string.find(link, "%[(.-)%]")
|
|
if nm and AA.ITEMNAME[string.lower(nm)] then return AA.ITEMNAME[string.lower(nm)] end
|
|
return nil
|
|
end
|
|
|
|
function AA.ScanBags(verbose)
|
|
if not AA.me then return end
|
|
local bags = { 0, 1, 2, 3, 4 }
|
|
if AA.bankOpen then
|
|
table.insert(bags, -1)
|
|
for b = 5, 10 do table.insert(bags, b) end
|
|
end
|
|
local found = 0
|
|
for _, bag in ipairs(bags) do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
for slot = 1, slots do
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local it = FindItemDef(link)
|
|
if it then
|
|
found = found + 1
|
|
local start, dur = GetContainerItemCooldown(bag, slot)
|
|
local exp = 0
|
|
if start and start > 0 and dur and dur > 30 then
|
|
local rem = start + dur - GetTime()
|
|
if rem > 0 then
|
|
exp = time() + rem
|
|
if exp > time() + it.cd + 60 then
|
|
-- client reported a nonsense start time: keep our stored value if sane, else assume a fresh use
|
|
local cur = AA.me.cds[it.key]
|
|
if cur and cur > time() and cur <= time() + it.cd + 60 then exp = cur else exp = time() + it.cd end
|
|
end
|
|
end
|
|
end
|
|
if verbose then
|
|
AA.Print(it.key .. " in bag " .. bag .. " slot " .. slot .. ": start=" .. tostring(start) .. " dur=" .. tostring(dur) .. " -> " .. (exp > 0 and AA.FormatTime(exp - time()) or "Ready"))
|
|
end
|
|
AA.SetCD(it.key, exp)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if verbose and found == 0 then
|
|
AA.Print("no tracked items found in bags" .. (AA.bankOpen and " / bank" or "") .. ". (Open the bank to scan it too.)")
|
|
end
|
|
return found
|
|
end
|
|
|
|
----------------------------------------------------------------------
|
|
-- scanning: character data, inventory and bank (for the alts manager)
|
|
----------------------------------------------------------------------
|
|
local QHEX = { ["9d9d9d"] = 0, ["ffffff"] = 1, ["1eff00"] = 2, ["0070dd"] = 3, ["a335ee"] = 4, ["ff8000"] = 5 }
|
|
|
|
local function LearnItem(id, link, tex)
|
|
AA.db.items = AA.db.items or {}
|
|
local it = AA.db.items[id]
|
|
if it and it.n and it.t and it.t ~= "" then return end
|
|
local _, _, nm = string.find(link, "%[(.-)%]")
|
|
local _, _, col = string.find(link, "|c%x%x(%x%x%x%x%x%x)|H")
|
|
local t = tex
|
|
if t then t = (string.gsub(t, "^Interface\\Icons\\", "")) end
|
|
AA.db.items[id] = { n = nm or ("item:" .. id), t = t or "", q = (col and QHEX[col]) or 1 }
|
|
end
|
|
|
|
-- returns aggregated list {{id,count},...} and a signature string, or nil if the containers are not available
|
|
local function ScanContainers(bags)
|
|
local order, agg = {}, {}
|
|
local slotsTotal = 0
|
|
for _, bag in ipairs(bags) do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
slotsTotal = slotsTotal + slots
|
|
for slot = 1, slots do
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local _, _, sid = string.find(link, "item:(%d+)")
|
|
local id = tonumber(sid)
|
|
if id then
|
|
local tex, count = GetContainerItemInfo(bag, slot)
|
|
count = count or 1
|
|
if count < 1 then count = 1 end
|
|
if not agg[id] then
|
|
agg[id] = 0
|
|
table.insert(order, id)
|
|
end
|
|
agg[id] = agg[id] + count
|
|
LearnItem(id, link, tex)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if slotsTotal == 0 then return nil end
|
|
local list, parts = {}, {}
|
|
for _, id in ipairs(order) do
|
|
table.insert(list, { id, agg[id] })
|
|
table.insert(parts, id .. ":" .. agg[id])
|
|
end
|
|
return list, table.concat(parts, "~")
|
|
end
|
|
|
|
-- equipped gear: slot -> "id:enchant:suffix:unique"
|
|
local function ScanEquip()
|
|
if not AA.me then return false end
|
|
local eq, parts, any = {}, {}, false
|
|
for slot = 0, 19 do
|
|
local link = GetInventoryItemLink("player", slot)
|
|
if link then
|
|
local _, _, ref = string.find(link, "item:(%-?%d+:%-?%d+:%-?%d+:%-?%d+)")
|
|
local _, _, sid = string.find(link, "item:(%d+)")
|
|
if ref and sid then
|
|
eq[slot] = ref
|
|
table.insert(parts, slot .. "=" .. ref)
|
|
LearnItem(tonumber(sid), link, GetInventoryItemTexture("player", slot))
|
|
any = true
|
|
end
|
|
end
|
|
end
|
|
if not any then return false end -- gear not loaded yet: keep what we have
|
|
local sig = table.concat(parts, "~")
|
|
if sig ~= AA.me.eqSig then
|
|
AA.me.equip = eq
|
|
AA.me.eqSig = sig
|
|
AA.me.eqTs = time()
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- profession skill levels from the skill window
|
|
local PROFN = {
|
|
["Alchemy"] = 1, ["Blacksmithing"] = 1, ["Enchanting"] = 1, ["Engineering"] = 1, ["Herbalism"] = 1,
|
|
["Leatherworking"] = 1, ["Mining"] = 1, ["Skinning"] = 1, ["Tailoring"] = 1,
|
|
["Cooking"] = 1, ["First Aid"] = 1, ["Fishing"] = 1, ["Survival"] = 1,
|
|
}
|
|
local function ScanSkills()
|
|
if not AA.me or not GetNumSkillLines then return false end
|
|
local n = GetNumSkillLines()
|
|
if not n or n == 0 then return false end
|
|
-- collapsed headers hide their skills: expand them (this only happens once)
|
|
for i = n, 1, -1 do
|
|
local name, header, isExp = GetSkillLineInfo(i)
|
|
if header and not isExp then ExpandSkillHeader(i) end
|
|
end
|
|
n = GetNumSkillLines()
|
|
AA.me.prof = AA.me.prof or {}
|
|
local seen, changed = {}, false
|
|
local section
|
|
for i = 1, n do
|
|
local name, header, _, rank, _, _, maxRank = GetSkillLineInfo(i)
|
|
if header then section = name end
|
|
if not header and name and PROFN[name] and not AA.BadProf(name, maxRank)
|
|
and not (name == "Survival" and section == "Class Skills") then
|
|
seen[name] = true
|
|
local p = AA.me.prof[name]
|
|
if not p then
|
|
p = {}
|
|
AA.me.prof[name] = p
|
|
end
|
|
if p.r ~= rank or p.m ~= maxRank then
|
|
p.r = rank
|
|
p.m = maxRank
|
|
p.ts = time()
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
for name, p in pairs(AA.me.prof) do
|
|
if not seen[name] and PROFN[name] then
|
|
AA.me.prof[name] = nil -- profession was unlearned
|
|
changed = true
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
AA.enterTime = 0
|
|
|
|
function AA.ScanSelf()
|
|
if not AA.me or not AA.db then return end
|
|
local changed = false
|
|
local lvl, gold = UnitLevel("player"), GetMoney()
|
|
local early = GetTime() - AA.enterTime < 12 -- right after login the client may still report 0
|
|
local metaChanged = false
|
|
if lvl and lvl > 0 and AA.me.level ~= lvl then
|
|
AA.me.level = lvl
|
|
metaChanged = true
|
|
end
|
|
if gold and not (gold == 0 and early) and AA.me.gold ~= gold then
|
|
AA.me.gold = gold
|
|
metaChanged = true
|
|
end
|
|
if metaChanged then
|
|
AA.me.dataTs = time()
|
|
changed = true
|
|
end
|
|
if ScanEquip() then changed = true end
|
|
if ScanSkills() then changed = true end
|
|
local list, sig = ScanContainers({ 0, 1, 2, 3, 4 })
|
|
if list and sig ~= AA.me.invSig then
|
|
AA.me.inv = list
|
|
AA.me.invSig = sig
|
|
AA.me.invTs = time()
|
|
changed = true
|
|
end
|
|
if AA.bankOpen then
|
|
local bags = { -1 }
|
|
for b = 5, 10 do table.insert(bags, b) end
|
|
local bl, bs = ScanContainers(bags)
|
|
if bl and bs ~= AA.me.bankSig then
|
|
AA.me.bank = bl
|
|
AA.me.bankSig = bs
|
|
AA.me.bankTs = time()
|
|
changed = true
|
|
end
|
|
end
|
|
if changed then
|
|
AA.fileDirty = true
|
|
if AA.RefreshManager then AA.RefreshManager() end
|
|
end
|
|
end
|
|
|
|
----------------------------------------------------------------------
|
|
-- data -> display
|
|
----------------------------------------------------------------------
|
|
function AA.SortedChars()
|
|
local list = {}
|
|
for k, c in pairs(AA.db.chars) do table.insert(list, k) end
|
|
table.sort(list, function(a, b)
|
|
if a == AA.myKey then return true end
|
|
if b == AA.myKey then return false end
|
|
return a < b
|
|
end)
|
|
return list
|
|
end
|
|
|
|
function AA.Entries(onlyCd)
|
|
local out = {}
|
|
local nowt = time()
|
|
for _, k in ipairs(AA.SortedChars()) do
|
|
local c = AA.db.chars[k]
|
|
local keys = {}
|
|
for key, exp in pairs(c.cds or {}) do table.insert(keys, key) end
|
|
table.sort(keys)
|
|
if table.getn(keys) > 0 or not onlyCd then
|
|
table.insert(out, { l = "|cff" .. AA.ClassHex(c.class) .. c.name .. "|r |cff808080" .. c.realm .. "|r", r = "" })
|
|
if table.getn(keys) == 0 then
|
|
table.insert(out, { l = " |cff808080(no cooldowns recorded)|r", r = "" })
|
|
end
|
|
for _, key in ipairs(keys) do
|
|
local exp = c.cds[key]
|
|
local rem = exp - nowt
|
|
local r
|
|
if exp <= 0 or rem <= 0 then
|
|
r = "|cff40ff40Ready|r"
|
|
elseif rem >= AA.DAY then
|
|
r = "|cffff6060" .. AA.FormatTime(rem) .. "|r"
|
|
else
|
|
r = "|cffffd100" .. AA.FormatTime(rem) .. "|r"
|
|
end
|
|
table.insert(out, { l = " " .. key, r = r })
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|