Загрузить файлы в «/»

This commit is contained in:
Ade
2026-10-08 06:06:50 +00:00
parent 4ef87a923c
commit 7363904535
5 changed files with 1026 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# Ade Alts
Alt manager for OctoWoW (1.12 client). It remembers everything about all your characters,
including those on other accounts, and shows it in one window.
- Profession cooldowns – Salt Shaker, Transmutes, Mooncloth; "ready" popup at login.
- Alt list – level, class icon, sorted by level or name.
- Inventory and bank of every alt, plus **item search** across all of them.
- Total gold of all alts.
- Professions – skill levels and known recipes (also searchable).
- Equipped gear of every alt (exclude cosmetic - tabard and shirt).
- Minimap button – docks into pfUI's addon button panel automatically.
- Settings – window scale, width, height, background opacity, background colour, cooldown-window opacity.
## Requirements
- OctoWoW / any 1.12 client.
- Optional but recommended: **[SuperWoW](https://github.com/balakethelock/SuperWoW)** – lets all accounts in the same game folder share data through `imports\AdeAlts.txt`. Without it, only cooldowns sync (guild / party / raid chat).
- Optional: pfUI (minimap button docking and skin).
## Installation
1. Put the `AdeAlts` folder into `<WoW folder>\Interface\AddOns\` (the folder must be named `AdeAlts` and contain `AdeAlts.toc`).
2. Log in with each character once so it gets cached (cached only inside one client\game folder).
3. Updating: replace the whole folder. Saved data is kept (`WTF\...\SavedVariables\AdeAlts.lua`).
## Usage
Left-click the minimap button (or `/ade`) opens the alt manager. Right-click the button forces a sync.
The button tooltip lists only characters that have cooldowns.
Open each profession window once per character (clear its filters first) so its recipes and cooldowns are recorded.
Fishing, Skinning and Herbalism have no recipes and need nothing.
## Slash commands
Every alias works the same: `/ade`, `/adealts`, `/pcd`, `/profcd`, `/cdt`.
| Command | What it does |
|---|---|
| `/ade` (also `show`, `toggle`, `alts`) | Open / close the alt manager window |
| `/ade cd` (also `cooldowns`) | Open / close the profession cooldowns window |
| `/ade find <text>` | Open the manager and search items and recipes across all alts |
| `/ade list` | Print all cooldowns to the chat frame |
| `/ade sync` | Sync now: shared file (SuperWoW) and a chat request to guild/party/raid |
| `/ade scan` | Rescan this character (level, gold, bags, gear, professions) and print what the client reports |
| `/ade status` | Show whether SuperWoW file sync is on and how many characters are known |
| `/ade export` | Open a text box with all data (copy it to move data by hand) |
| `/ade import` | Open a text box to paste exported data into |
| `/ade minimap` | Show / hide the minimap button |
| `/ade alert` | Turn the login "cooldown ready" popup on / off |
| `/ade test` | Show the "ready" popup now (also when nothing is ready) |
| `/ade debug` | Print raw cooldown values of tracked bag items and stored cooldowns (for troubleshooting) |
| `/ade reset` | Set all cooldowns of the current character to Ready |
| `/ade del <name>` | Remove a cached character (not the one you are logged in on) |
## How data is shared
Each character stores its own record (gold, bags, bank, gear, professions, cooldowns). With SuperWoW all accounts
read and write one shared file, `<WoW folder>\imports\AdeAlts.txt`, and the newest data per character wins.
Without SuperWoW, cooldowns are also exchanged through guild / party / raid addon messages.
## Notes
- Bank contents are updated when you open the bank.
- Salt Shaker can occur bug, use /ade scan if it's occur and report if /ade scan didn't help.
- Written for Lua 5.0 (1.12 client); the code is split into several files that share one global table `AdeAlts`.
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
+125
View File
@@ -0,0 +1,125 @@
-- Ade Alts: core
AdeAlts = {}
local AA = AdeAlts
-- AdeAlts : profession cooldown tracker for Turtle WoW / 1.12 clients (Lua 5.0)
-- Tracks Salt Shaker, Transmutes, Mooncloth (and any "Transmute: ..." recipe) per character.
-- Command: /pcd Minimap button: auto-docked by pfUI's addon button panel.
AA.ADDON = "AdeAlts"
AA.VERSION = "1.0"
AA.PREFIX = "PCDT"
AA.DAY = 86400
local HOUR = 3600
-- Fallback durations (seconds). The real remaining time is read from the recipe tooltip when possible.
AA.DEFAULTS = {
["Mooncloth"] = 4 * AA.DAY,
["Transmute: Arcanite"] = 2 * AA.DAY,
["Transmute: Iron to Gold"] = 2 * AA.DAY,
["Transmute: Mithril to Truesilver"] = 2 * AA.DAY,
}
-- Items tracked through their bag cooldown.
AA.ITEMS = {
[15846] = { key = "Salt Shaker", cd = 3 * AA.DAY },
}
-- max possible cooldown per item key; anything beyond it is a bogus client value
local ITEMCD = {}
AA.ITEMNAME = {}
for id, it in pairs(AA.ITEMS) do ITEMCD[it.key] = it.cd; AA.ITEMNAME[string.lower(it.key)] = it end
function AA.ClampRecord(c)
local changed = false
local lim = time()
for key, cd in pairs(ITEMCD) do
local e = c.cds and c.cds[key]
if e and e > lim + cd + 60 then
c.cds[key] = lim + cd
changed = true
end
end
return changed
end
-- the hunter "Survival" talent tree is also a skill line (rank 1/1); the real profession has a max of 75+
function AA.BadProf(name, maxr)
return name == "Survival" and (not maxr or maxr <= 1)
end
-- professions without a recipe list (nothing to scan)
AA.NORECIPES = { Fishing = 1, Skinning = 1, Herbalism = 1 }
AA.timers = {}
AA.prov = {}
local alerted = {}
AA.scanning, AA.dirty = false, false
AA.lastScan, AA.lastReply, AA.lastBroadcast = 0, 0, 0
AA.bagDirty = false
AA.loaded = false
AA.fileDirty, AA.lastFileWrite = false, 0
AA.LEGACY = "ProfCDTracker"
AA.FILE = "AdeAlts" -- <WoW folder>\imports\AdeAlts.txt (SuperWoW)
----------------------------------------------------------------------
-- helpers
----------------------------------------------------------------------
function AA.Print(m)
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccAdeAlts|r: " .. m)
end
function AA.After(sec, fn)
table.insert(AA.timers, { t = GetTime() + sec, fn = fn })
end
function AA.IsTracked(name)
return AA.DEFAULTS[name] or string.find(name, "^Transmute")
end
function AA.FormatTime(s)
s = math.floor(s)
local d = math.floor(s / AA.DAY)
local h = math.floor(math.mod(s, AA.DAY) / HOUR)
local m = math.floor(math.mod(s, HOUR) / 60)
if d > 0 then return d .. "d " .. h .. "h" end
if h > 0 then return h .. "h " .. m .. "m" end
if m > 0 then return m .. "m" end
return s .. "s"
end
function AA.ClassHex(class)
local col = RAID_CLASS_COLORS and class and RAID_CLASS_COLORS[class]
if col then
return string.format("%02x%02x%02x", math.floor(col.r * 255), math.floor(col.g * 255), math.floor(col.b * 255))
end
return "ffffff"
end
function AA.ParseDuration(text)
local total, found = 0, false
for n, u in string.gfind(text, "(%d+)%s*(%a+)") do
n = tonumber(n)
u = string.lower(u)
local fm
if string.find(u, "^d") then fm = AA.DAY
elseif string.find(u, "^h") then fm = HOUR
elseif string.find(u, "^m") then fm = 60
elseif string.find(u, "^s") then fm = 1 end
if AA.f then total = total + n * AA.f; found = true end
end
if found then return total end
return nil
end
function AA.SetCD(key, exp)
if not AA.me then return end
exp = math.floor(exp)
local old = AA.me.cds[key]
if old == nil or math.abs(old - exp) > 5 or ((old == 0) ~= (exp == 0)) then
AA.me.cds[key] = exp
AA.me.updated = time()
AA.dirty = true
end
end
+271
View File
@@ -0,0 +1,271 @@
-- Ade Alts: events
local AA = AdeAlts
----------------------------------------------------------------------
-- driver: timers, ready alerts, delayed broadcast
----------------------------------------------------------------------
local driver = CreateFrame("Frame")
local acc, filet, metat = 0, 0, 0
driver:SetScript("OnUpdate", function()
acc = acc + arg1
if acc < 1 then return end
acc = 0
local now = GetTime()
for i = table.getn(AA.timers), 1, -1 do
local t = AA.timers[i]
if t and t.t <= now then
table.remove(AA.timers, i)
t.fn()
end
end
if not AA.me then return end
if AA.bagDirty then
AA.bagDirty = false
AA.ScanBags()
AA.ScanSelf()
end
if AA.fileDirty and now - AA.lastFileWrite > 10 then
AA.FileSync()
end
-- ready alerts for the logged-in character
local nowt = time()
for key, exp in pairs(AA.me.cds) do
if exp > 0 and exp <= nowt then
AA.Print("|cff40ff40" .. key .. " is ready!|r")
AA.me.cds[key] = 0
AA.me.updated = time()
AA.dirty = true
end
end
metat = metat + 1
if metat >= 30 then
metat = 0
AA.ScanSelf()
end
filet = filet + 1
if filet >= 20 then
filet = 0
AA.FileMerge()
end
if AA.dirty and now - AA.lastBroadcast > 3 then
AA.dirty = false
AA.Broadcast()
AA.FileSync()
end
end)
----------------------------------------------------------------------
-- events
----------------------------------------------------------------------
local ev = CreateFrame("Frame")
ev:RegisterEvent("ADDON_LOADED")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("TRADE_SKILL_SHOW")
ev:RegisterEvent("TRADE_SKILL_UPDATE")
ev:RegisterEvent("BAG_UPDATE")
ev:RegisterEvent("BAG_UPDATE_COOLDOWN")
ev:RegisterEvent("BANKFRAME_OPENED")
ev:RegisterEvent("BANKFRAME_CLOSED")
ev:RegisterEvent("PLAYER_MONEY")
ev:RegisterEvent("PLAYER_LEVEL_UP")
ev:RegisterEvent("PLAYER_LOGOUT")
ev:RegisterEvent("UNIT_INVENTORY_CHANGED")
ev:RegisterEvent("SKILL_LINES_CHANGED")
ev:RegisterEvent("CRAFT_SHOW")
ev:RegisterEvent("CRAFT_UPDATE")
ev:RegisterEvent("CHAT_MSG_ADDON")
ev:SetScript("OnEvent", function()
if event == "ADDON_LOADED" then
if arg1 == AA.ADDON then
if not AdeAltsDB then
-- migrate from the old ProfCD Tracker saved variables, if present
AdeAltsDB = ProfCDTrackerDB or {}
end
ProfCDTrackerDB = nil
AA.db = AdeAltsDB
AA.db.chars = AA.db.chars or {}
AA.db.items = AA.db.items or {}
-- drop the hunter Survival talent line that older versions stored as a profession
for _, ch in pairs(AA.db.chars) do
local sv = ch.prof and ch.prof.Survival
if sv and AA.BadProf("Survival", sv.m) then ch.prof.Survival = nil end
end
AA.ApplyConfig()
AA.db.angle = AA.db.angle or 200
if AA.db.hidden then AA.btn:Hide() end
AA.PlaceButton()
end
elseif event == "PLAYER_ENTERING_WORLD" then
if not AA.db then return end
if not AA.loaded then
AA.loaded = true
local name = UnitName("player")
local realm = GetRealmName()
local _, class = UnitClass("player")
AA.myKey = name .. "-" .. realm
AA.me = AA.db.chars[AA.myKey]
if not AA.me then
AA.me = { name = name, realm = realm, cds = {} }
AA.db.chars[AA.myKey] = AA.me
end
AA.me.class = class
AA.me.remote = nil
AA.me.updated = AA.me.updated or time()
if AA.ClampRecord(AA.me) then AA.me.updated = time(); AA.dirty = true end
AA.me.cds = AA.me.cds or {}
AA.StyleButton()
AA.enterTime = GetTime()
AA.After(2, function() AA.ScanBags(); AA.ScanSelf() end)
AA.After(5, function() AA.ScanSelf() end)
AA.After(8, function() AA.ScanSelf() end)
AA.After(13, function() AA.ScanSelf() end)
AA.After(20, function() AA.ScanSelf() end)
AA.After(4, AA.DockInPfUI)
AA.After(10, function() AA.Send("R"); AA.Broadcast() end)
AA.After(3, AA.FileSync)
AA.After(6, AA.ShowReadyAlert)
end
AA.bagDirty = true
elseif event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_UPDATE" then
AA.ScanTradeSkills()
elseif event == "CRAFT_SHOW" or event == "CRAFT_UPDATE" then
AA.ScanCrafts()
elseif event == "UNIT_INVENTORY_CHANGED" then
if arg1 == "player" then AA.bagDirty = true end
elseif event == "SKILL_LINES_CHANGED" then
AA.bagDirty = true
elseif event == "BANKFRAME_OPENED" then
AA.bankOpen = true
AA.bagDirty = true
elseif event == "BANKFRAME_CLOSED" then
AA.ScanSelf()
AA.bankOpen = false
elseif event == "PLAYER_MONEY" or event == "PLAYER_LEVEL_UP" then
AA.bagDirty = true
elseif event == "PLAYER_LOGOUT" then
if AA.me then AA.FileSync() end
elseif event == "BAG_UPDATE" or event == "BAG_UPDATE_COOLDOWN" then
AA.bagDirty = true
elseif event == "CHAT_MSG_ADDON" then
if arg1 == AA.PREFIX and AA.db and arg4 ~= UnitName("player") then
if arg2 == "R" then
if AA.me and GetTime() - AA.lastReply > 5 then
AA.lastReply = GetTime()
AA.Broadcast()
end
else
AA.ReceiveRecord(arg2)
AA.Refresh()
end
end
end
end)
----------------------------------------------------------------------
-- slash commands
----------------------------------------------------------------------
SLASH_PROFCD1 = "/pcd"
SLASH_PROFCD2 = "/profcd"
SLASH_PROFCD3 = "/cdt"
SLASH_PROFCD4 = "/ade"
SLASH_PROFCD5 = "/adealts"
SlashCmdList["PROFCD"] = function(msg)
msg = msg or ""
local _, _, cmd, arg = string.find(msg, "^%s*(%S*)%s*(.-)%s*$")
cmd = string.lower(cmd or "")
if cmd == "" or cmd == "show" or cmd == "toggle" or cmd == "alts" then
AA.ToggleManager()
elseif cmd == "cd" or cmd == "cooldowns" then
AA.ToggleWindow()
elseif cmd == "find" and arg ~= "" then
AA.sbox:SetText(arg)
if not AA.M:IsVisible() then AA.M:Show() end
elseif cmd == "list" then
for _, e in ipairs(AA.Entries()) do AA.Print(e.l .. " " .. e.r) end
elseif cmd == "sync" then
AA.Send("R")
AA.Broadcast()
AA.FileSync()
AA.Print("synced (shared file: " .. ((ImportFile and ExportFile) and "on" or "off") .. ", chat request sent).")
elseif cmd == "scan" then
AA.Print("client reports: level=" .. tostring(UnitLevel("player")) .. " gold(copper)=" .. tostring(GetMoney()))
AA.ScanSelf()
AA.ScanBags(true)
AA.FileSync()
elseif cmd == "status" then
if ImportFile and ExportFile then
AA.Print("SuperWoW file sync: |cff40ff40ON|r (imports\\" .. AA.FILE .. ".txt, shared by all accounts in this game folder)")
else
AA.Print("SuperWoW file sync: |cffff6060OFF|r (SuperWoW not detected). Only guild/party/raid chat sync works.")
end
local n = 0
for k in pairs(AA.db.chars) do n = n + 1 end
AA.Print(n .. " character(s) known.")
elseif cmd == "export" then
AA.ShowIO(true)
elseif cmd == "import" then
AA.ShowIO(false)
elseif cmd == "minimap" then
AA.db.hidden = not AA.db.hidden
if AA.db.hidden then AA.btn:Hide() else AA.btn:Show(); AA.After(1, AA.DockInPfUI) end
AA.Print("minimap button " .. (AA.db.hidden and "hidden" or "shown") .. ".")
elseif cmd == "alert" then
AA.db.noAlert = not AA.db.noAlert
AA.Print("login alert " .. (AA.db.noAlert and "disabled" or "enabled") .. ".")
elseif cmd == "test" then
AA.ShowReadyAlert(true)
elseif cmd == "debug" then
local found = 0
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot)
local it = AA.ItemFromLink(link)
if it then
found = found + 1
local st, du = GetContainerItemCooldown(bag, slot)
AA.Print(it.key .. " in bag " .. bag .. " slot " .. slot .. ": start=" .. tostring(st) .. " duration=" .. tostring(du) .. " now=" .. GetTime() .. " remaining=" .. tostring((st or 0) + (du or 0) - GetTime()))
end
end
end
if found == 0 then AA.Print("no tracked item found in bags 0-4 (is it in the bank?)") end
for key, exp in pairs(AA.me.cds) do
AA.Print("stored " .. key .. ": " .. (exp > 0 and (AA.FormatTime(exp - time()) .. " left") or "ready"))
end
AA.Print("file sync: " .. ((ImportFile and ExportFile) and "on" or "off"))
elseif cmd == "reset" then
for k in pairs(AA.me.cds) do AA.me.cds[k] = 0 end
AA.dirty = true
AA.Print("cooldowns of this character reset to Ready.")
elseif cmd == "del" and arg ~= "" then
local a = string.lower(arg)
local n = 0
for k, c in pairs(AA.db.chars) do
if k ~= AA.myKey and (string.lower(c.name) == a or string.lower(k) == a) then
AA.db.chars[k] = nil
n = n + 1
end
end
AA.Print("removed " .. n .. " character(s).")
AA.Refresh()
else
AA.Print("/ade - alts window | cd (cooldowns) | find <item> | list | sync | scan | status | debug | export | import | minimap | alert | reset | del <name>")
end
end
+111
View File
@@ -0,0 +1,111 @@
-- Ade Alts: minimap
local AA = AdeAlts
----------------------------------------------------------------------
-- minimap button (pfUI docks it automatically into its addon panel)
----------------------------------------------------------------------
AA.btn = CreateFrame("Button", "AdeAltsMinimapButton", Minimap)
AA.btn:SetWidth(31)
AA.btn:SetHeight(31)
AA.btn:SetFrameStrata("MEDIUM")
AA.btn:SetFrameLevel(8)
AA.btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
AA.btn:RegisterForDrag("LeftButton")
local icon = AA.btn:CreateTexture(nil, "BACKGROUND")
icon:SetTexture("Interface\\Icons\\Trade_Alchemy")
local border = AA.btn:CreateTexture(nil, "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
function AA.StyleButton()
if pfUI and pfUI.api and pfUI.api.CreateBackdrop then
-- pfUI look: flat icon inside a pfUI backdrop, same as other docked buttons
if not AA.btn.pcdBackdrop then
AA.btn.pcdBackdrop = true
pcall(pfUI.api.CreateBackdrop, AA.btn, nil, true)
end
icon:SetDrawLayer("ARTWORK")
icon:ClearAllPoints()
icon:SetPoint("TOPLEFT", AA.btn, "TOPLEFT", 3, -3)
icon:SetPoint("BOTTOMRIGHT", AA.btn, "BOTTOMRIGHT", -3, 3)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
border:Hide()
AA.btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
elseif pfUI then
icon:SetDrawLayer("ARTWORK")
icon:ClearAllPoints()
icon:SetAllPoints(AA.btn)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
border:Hide()
AA.btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
else
icon:SetDrawLayer("BACKGROUND")
icon:ClearAllPoints()
icon:SetWidth(20); icon:SetHeight(20)
icon:SetPoint("TOPLEFT", AA.btn, "TOPLEFT", 6, -5)
icon:SetTexCoord(0, 1, 0, 1)
border:ClearAllPoints()
border:SetWidth(53); border:SetHeight(53)
border:SetPoint("TOPLEFT", AA.btn, "TOPLEFT", 0, 0)
border:Show()
AA.btn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
end
end
AA.StyleButton()
function AA.PlaceButton()
if AA.btn:GetParent() ~= Minimap then return end -- docked by pfUI
local angle = (AA.db and AA.db.angle) or 200
local r = Minimap:GetWidth() / 2 + 6
local rad = math.rad(angle)
AA.btn:ClearAllPoints()
AA.btn:SetPoint("CENTER", Minimap, "CENTER", math.cos(rad) * r, math.sin(rad) * r)
end
AA.PlaceButton()
AA.btn:SetScript("OnDragStart", function()
if this:GetParent() ~= Minimap then return end
this:SetScript("OnUpdate", function()
local mx, my = GetCursorPosition()
local sc = Minimap:GetEffectiveScale()
local cx, cy = Minimap:GetCenter()
AA.db.angle = math.deg(math.atan2(my / sc - cy, mx / sc - cx))
AA.PlaceButton()
end)
end)
AA.btn:SetScript("OnDragStop", function() this:SetScript("OnUpdate", nil) end)
AA.btn:SetScript("OnClick", function()
if arg1 == "RightButton" then
AA.Send("R")
AA.Broadcast()
AA.FileSync()
AA.Print("synced.")
else
AA.ToggleManager()
end
end)
AA.btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:AddLine("Ade Alts - cooldowns")
if AA.db then
local list = AA.Entries(true)
if table.getn(list) == 0 then
GameTooltip:AddLine("|cff808080No cooldowns recorded yet|r")
end
for _, e in ipairs(list) do
GameTooltip:AddDoubleLine(e.l, e.r, 1, 1, 1, 1, 1, 1)
end
end
GameTooltip:AddLine("|cff808080Left: window Right: sync Drag: move|r")
GameTooltip:Show()
end)
AA.btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
function AA.DockInPfUI()
if pfUI and pfUI.addonbuttons and pfUI.addonbuttons.ProcessButtons then
pcall(function() pfUI.addonbuttons:ProcessButtons() end)
end
end
+448
View File
@@ -0,0 +1,448 @@
-- 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