forever-addons/ProfessionPricer/ProfessionPricer.lua

710 lines
29 KiB
Lua
Executable File

local ADDON = ...
local app = CreateFrame("Frame")
local DB
local scanButton
local cacheRevision = 0
local updateCraftPriceUI
local function say(message)
print("|cff33ff99Profession Pricer:|r " .. message)
end
local function comma(value)
local result = tostring(math.floor(tonumber(value) or 0))
while true do
local nextResult, count = result:gsub("^(-?%d+)(%d%d%d)", "%1,%2")
result = nextResult
if count == 0 then return result end
end
end
local GOLD = "|TInterface\\MoneyFrame\\UI-GoldIcon:0:0:2:0|t"
local SILVER = "|TInterface\\MoneyFrame\\UI-SilverIcon:0:0:2:0|t"
local COPPER = "|TInterface\\MoneyFrame\\UI-CopperIcon:0:0:2:0|t"
local function money(copper)
copper = math.max(0, math.floor(tonumber(copper) or 0))
local gold = math.floor(copper / 10000)
local silver = math.floor((copper % 10000) / 100)
local coins = copper % 100
local parts = {}
if gold > 0 then parts[#parts + 1] = gold .. GOLD end
if silver > 0 or gold > 0 then parts[#parts + 1] = silver .. SILVER end
parts[#parts + 1] = coins .. COPPER
return table.concat(parts, " ")
end
local function age(timestamp)
if not timestamp then return "unknown" end
local seconds = math.max(0, time() - timestamp)
if seconds < 60 then return "<1m" end
if seconds < 3600 then return math.floor(seconds / 60) .. "m" end
if seconds < 86400 then return math.floor(seconds / 3600) .. "h" end
return math.floor(seconds / 86400) .. "d"
end
local function countPrices()
local count = 0
for _ in pairs(DB.prices) do count = count + 1 end
return count
end
local function cachedPrice(itemID)
local entry = DB and DB.prices and DB.prices[tonumber(itemID)]
return entry and tonumber(entry.price) or nil
end
local function savePrice(itemID, price, source)
itemID = tonumber(itemID)
price = tonumber(price)
if not itemID or not price or price <= 0 then return end
price = math.floor(price)
if price <= 0 then return end
local old = DB.prices[itemID]
DB.prices[itemID] = {price = price, updated = time(), source = source}
if not old or old.price ~= price then cacheRevision = cacheRevision + 1 end
end
local function lowestCommodityPrice(itemID)
if not (C_AuctionHouse and C_AuctionHouse.GetNumCommoditySearchResults and C_AuctionHouse.GetCommoditySearchResultInfo) then return nil end
local ok, count = pcall(C_AuctionHouse.GetNumCommoditySearchResults, itemID)
if not ok or not count then return nil end
local best
for i = 1, count do
local success, result = pcall(C_AuctionHouse.GetCommoditySearchResultInfo, itemID, i)
local price = success and result and tonumber(result.unitPrice)
if price and price > 0 and (not best or price < best) then best = price end
end
return best
end
local function lowestItemPrice(itemKey)
if not (C_AuctionHouse and C_AuctionHouse.GetNumItemSearchResults and C_AuctionHouse.GetItemSearchResultInfo) then return nil end
local ok, count = pcall(C_AuctionHouse.GetNumItemSearchResults, itemKey)
if not ok or not count then return nil end
local best
for i = 1, count do
local success, result = pcall(C_AuctionHouse.GetItemSearchResultInfo, itemKey, i)
local price = success and result and tonumber(result.buyoutAmount)
if price and price > 0 and (not best or price < best) then best = price end
end
return best
end
local browse = {active = false, token = 0}
local updateScanButton
local function countSeen()
local count = 0
for _ in pairs(browse.seen or {}) do count = count + 1 end
return count
end
local function endBrowse(reason, completed)
if not browse.active then return end
browse.active = false
browse.token = browse.token + 1
local count = countSeen()
for itemID, price in pairs(browse.prices) do savePrice(itemID, price, "browse") end
if completed then
DB.lastBrowseScan = time()
DB.lastBrowseItems = count
end
updateScanButton()
say((completed and "AH browse scan complete: " or "AH browse scan stopped: ") .. comma(count) .. " item summaries cached (" .. reason .. ").")
end
local function cacheBrowseResults()
if not (C_AuctionHouse and C_AuctionHouse.GetBrowseResults) then return 0, 0 end
local ok, results = pcall(C_AuctionHouse.GetBrowseResults)
if not ok or type(results) ~= "table" then return 0, 0 end
local added = 0
for _, result in ipairs(results) do
local key = result.itemKey
local itemID = key and tonumber(key.itemID)
local price = tonumber(result.minPrice)
if itemID and price and price > 0 then
if browse.active then
local signature = itemID .. ":" .. tostring(key.itemLevel or 0) .. ":" .. tostring(key.itemSuffix or 0) .. ":" .. tostring(key.battlePetSpeciesID or 0)
if not browse.seen[signature] then
browse.seen[signature] = true
added = added + 1
end
local prior = browse.prices[itemID]
if not prior or price < prior then browse.prices[itemID] = price end
else
local old = DB.prices[itemID]
if not old or not old.price or price < old.price
or (old.source == "browse" and old.updated and time() - old.updated > 60) then
savePrice(itemID, price, "browse")
end
end
end
end
return added, #results
end
local function requestMoreBrowse(token)
if not browse.active or browse.token ~= token then return end
if C_AuctionHouse.IsThrottledMessageSystemReady then
local ok, ready = pcall(C_AuctionHouse.IsThrottledMessageSystemReady)
if ok and not ready then
C_Timer.After(0.75, function() requestMoreBrowse(token) end)
return
end
end
browse.waiting = true
local ok = pcall(C_AuctionHouse.RequestMoreBrowseResults)
if not ok then endBrowse("request failed", false) end
end
local function browseResultsUpdated()
local added, total = cacheBrowseResults()
if not browse.active or not browse.waiting then return end
browse.waiting = false
browse.lastActivity = time()
browse.pages = browse.pages + 1
updateScanButton()
if C_AuctionHouse.HasFullBrowseResults then
local ok, full = pcall(C_AuctionHouse.HasFullBrowseResults)
if ok and full then endBrowse("full results", true); return end
end
if total == browse.lastCount and added == 0 then
browse.stable = browse.stable + 1
else
browse.stable = 0
end
browse.lastCount = total
if browse.stable >= 3 then endBrowse("no more results", true); return end
if not C_AuctionHouse.RequestMoreBrowseResults then
endBrowse("pagination unavailable", false)
return
end
local token = browse.token
C_Timer.After(0.35, function() requestMoreBrowse(token) end)
end
local function browseWatchdog(token)
if not browse.active or browse.token ~= token then return end
if time() - browse.lastActivity >= 20 then
endBrowse("no response from Auction House", false)
else
C_Timer.After(5, function() browseWatchdog(token) end)
end
end
local function startBrowseScan()
if browse.active then say("AH browse scan is already running (" .. comma(countSeen()) .. " summaries found).") return end
if not (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then say("Open the Auction House before scanning.") return end
if not (C_AuctionHouse and C_AuctionHouse.SendBrowseQuery and C_AuctionHouse.GetBrowseResults) then
say("AH browse scanning API is unavailable on this client.")
return
end
browse.token = browse.token + 1
browse.active = true
browse.waiting = true
browse.seen = {}
browse.prices = {}
browse.pages = 0
browse.stable = 0
browse.lastCount = 0
browse.lastActivity = time()
updateScanButton()
local filters = {}
if Enum and Enum.AuctionHouseFilter then
for _, name in ipairs({"PoorQuality", "CommonQuality", "UncommonQuality", "RareQuality", "EpicQuality", "LegendaryQuality", "ArtifactQuality"}) do
local filter = Enum.AuctionHouseFilter[name]
if filter ~= nil then filters[#filters + 1] = filter end
end
end
local sorts = {}
if Enum and Enum.AuctionHouseSortOrder then
if Enum.AuctionHouseSortOrder.Price ~= nil then
sorts[#sorts + 1] = {sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false}
end
if Enum.AuctionHouseSortOrder.Name ~= nil then
sorts[#sorts + 1] = {sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false}
end
end
local ok, err = pcall(C_AuctionHouse.SendBrowseQuery, {searchString = "", sorts = sorts, filters = filters})
if not ok then endBrowse("query failed: " .. tostring(err), false); return end
say("starting AH browse scan...")
local token = browse.token
C_Timer.After(5, function() browseWatchdog(token) end)
end
updateScanButton = function()
if not scanButton then return end
scanButton:SetText(browse.active and "Scanning..." or "Scan AH")
if browse.active then
scanButton.status:SetText("|cffffff66" .. comma(countSeen()) .. " found|r")
elseif DB.lastBrowseScan then
scanButton.status:SetText("|cff33ff66Last: " .. age(DB.lastBrowseScan) .. " ago|r")
else
scanButton.status:SetText("|cffaaaaaaNot scanned|r")
end
end
local function createScanButton()
if scanButton or not AuctionHouseFrame then return end
local button = CreateFrame("Button", "ProfessionPricerScanButton", AuctionHouseFrame, "UIPanelButtonTemplate")
button:SetSize(92, 24)
if AuctionHouseFrameAuctionsTab then
button:SetPoint("LEFT", AuctionHouseFrameAuctionsTab, "RIGHT", 8, 0)
elseif AuctionHouseFrame.BuyTab then
button:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMLEFT", 310, 4)
else
button:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMLEFT", 305, 4)
end
button:SetFrameLevel((AuctionHouseFrame:GetFrameLevel() or 1) + 20)
local status = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
status:SetPoint("TOP", button, "BOTTOM", 0, -1)
button.status = status
button:SetScript("OnClick", startBrowseScan)
button:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText("Profession Pricer AH Scan")
if browse.active then
GameTooltip:AddLine(comma(countSeen()) .. " item summaries discovered", 1, 1, 1)
elseif DB.lastBrowseScan then
GameTooltip:AddLine("Last scan: " .. age(DB.lastBrowseScan) .. " ago", 1, 1, 1)
GameTooltip:AddLine(comma(DB.lastBrowseItems or 0) .. " item summaries cached", 1, 1, 1)
else
GameTooltip:AddLine("No scan completed yet.", 1, 1, 1)
end
GameTooltip:Show()
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
scanButton = button
updateScanButton()
end
local replicate = {active = false, token = 0}
local replicationUpdated
local function finishReplication()
if not replicate.active then return end
local count = 0
for itemID, price in pairs(replicate.prices) do
savePrice(itemID, price, "replicate")
count = count + 1
end
DB.lastFullScan = time()
DB.lastFullItems = count
DB.lastFullAuctions = replicate.auctions
replicate.active = false
replicate.token = replicate.token + 1
say("full AH scan complete: " .. comma(count) .. " item prices from " .. comma(replicate.auctions) .. " auctions.")
end
local function processReplicationBatch(token)
if not replicate.active or replicate.token ~= token then return end
local stop = math.min(replicate.index + 249, replicate.total - 1)
while replicate.index <= stop do
local ok, _, _, quantity, _, _, _, _, _, _, buyout, _, _, _, _, _, _, itemID = pcall(C_AuctionHouse.GetReplicateItemInfo, replicate.index)
if ok and itemID and tonumber(buyout) and buyout > 0 then
quantity = math.max(1, tonumber(quantity) or 1)
local unitPrice = math.floor(buyout / quantity)
if unitPrice > 0 and (not replicate.prices[itemID] or unitPrice < replicate.prices[itemID]) then
replicate.prices[itemID] = unitPrice
end
replicate.auctions = replicate.auctions + 1
end
replicate.index = replicate.index + 1
end
if replicate.index >= replicate.total then
finishReplication()
else
C_Timer.After(0.02, function() processReplicationBatch(token) end)
end
end
replicationUpdated = function()
if not replicate.active or not replicate.waiting then return end
local ok, count = pcall(C_AuctionHouse.GetNumReplicateItems)
if not ok then
replicate.active = false
say("could not read replicated AH snapshot.")
return
end
if not count or count <= 0 then return end
replicate.waiting = false
replicate.total = count
replicate.index = 0
say("AH snapshot received: " .. comma(count) .. " auctions; building price cache...")
processReplicationBatch(replicate.token)
end
local function startFullScan()
if replicate.active then say("full AH scan is already running.") return end
if not (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then say("Open the Auction House before scanning.") return end
if not (C_AuctionHouse and C_AuctionHouse.ReplicateItems and C_AuctionHouse.GetNumReplicateItems and C_AuctionHouse.GetReplicateItemInfo) then
say("AH replication API is unavailable on this client.")
return
end
replicate.token = replicate.token + 1
replicate.active = true
replicate.waiting = true
replicate.index = 0
replicate.total = 0
replicate.auctions = 0
replicate.prices = {}
local ok, err = pcall(C_AuctionHouse.ReplicateItems)
if not ok then
replicate.active = false
say("AH snapshot request failed: " .. tostring(err))
return
end
say("requesting full AH snapshot...")
local token = replicate.token
local tries = 0
local function poll()
if not replicate.active or not replicate.waiting or replicate.token ~= token then return end
tries = tries + 1
replicationUpdated()
if not replicate.waiting then return end
if tries >= 30 then
replicate.active = false
say("no replicated AH snapshot arrived; scan stopped.")
else
C_Timer.After(0.5, poll)
end
end
C_Timer.After(0.5, poll)
end
local craft = {owner = nil, frame = nil, reagentLines = {}, recipeID = nil, revision = -1}
local function selectedRecipe()
local form = ProfessionsFrame and ProfessionsFrame.CraftingPage and ProfessionsFrame.CraftingPage.SchematicForm
if not form or type(form.GetRecipeInfo) ~= "function" then return nil, form end
local ok, info = pcall(form.GetRecipeInfo, form)
if not ok or not info then return nil, form end
return info.recipeID, form
end
local function recipePricing(recipeID)
if not (recipeID and C_TradeSkillUI and type(C_TradeSkillUI.GetRecipeSchematic) == "function") then return nil end
local ok, schematic = pcall(C_TradeSkillUI.GetRecipeSchematic, recipeID, false)
if not ok or not schematic then return nil end
local result = {cost = 0, reagents = {}, outputID = schematic.outputItemID, outputQty = tonumber(schematic.quantityMin) or 1}
for _, slot in ipairs(schematic.reagentSlotSchematics or {}) do
local quantity = tonumber(slot.quantityRequired) or 0
local reagent = slot.reagents and slot.reagents[1]
if reagent and reagent.itemID and quantity > 0 then
local price = cachedPrice(reagent.itemID)
result.reagents[#result.reagents + 1] = {quantity = quantity, price = price}
if price then result.cost = result.cost + price * quantity else result.missing = true end
end
end
local outputPrice = result.outputID and cachedPrice(result.outputID)
result.outputValue = outputPrice and outputPrice * result.outputQty or nil
return result
end
local SORT_MODES = {"default", "cost", "profit"}
local SORT_LABELS = {default = "Default", cost = "Craft Cost: low first", profit = "Profit/Craft: high first"}
local recipeSort = {scrollBox = nil, button = nil, revision = -1}
local function sortedRecipeProvider(source)
if not DB or DB.sortMode == "default" or not source or type(CreateTreeDataProvider) ~= "function"
or type(source.EnumerateEntireRange) ~= "function" then
return source
end
local rows, seen = {}, {}
local ok = pcall(function()
for _, node in source:EnumerateEntireRange() do
local data = node:GetData()
local info = data and data.recipeInfo
if info and info.recipeID and not info.favoritesInstance and not seen[info.recipeID] then
seen[info.recipeID] = true
local pricing = recipePricing(info.recipeID)
local score
if pricing and not pricing.missing then
if DB.sortMode == "cost" then
score = pricing.cost
elseif pricing.outputValue then
score = pricing.outputValue - pricing.cost
end
end
rows[#rows + 1] = {info = info, score = score, learned = info.learned ~= false}
end
end
end)
if not ok or #rows == 0 then return source end
table.sort(rows, function(a, b)
-- Keep recipes the character cannot craft after learned recipes.
if a.learned ~= b.learned then return a.learned end
if a.score == nil and b.score ~= nil then return false end
if b.score == nil and a.score ~= nil then return true end
if a.score ~= nil and b.score ~= nil and a.score ~= b.score then
if DB.sortMode == "cost" then return a.score < b.score end
return a.score > b.score
end
local aName = (a.info.name or ""):lower()
local bName = (b.info.name or ""):lower()
if aName ~= bName then return aName < bName end
return a.info.recipeID < b.info.recipeID
end)
local built, flat = pcall(function()
local provider = CreateTreeDataProvider()
local root = provider:GetRootNode()
for _, row in ipairs(rows) do root:Insert({recipeInfo = row.info, order = 0}) end
return provider
end)
return built and flat or source
end
local function refreshSortedRecipes()
local page = ProfessionsFrame and ProfessionsFrame.CraftingPage
if not page or not page.professionInfo or type(page.Init) ~= "function" then return end
recipeSort.revision = cacheRevision
local ok, err = pcall(page.Init, page, page.professionInfo)
if not ok then say("could not refresh recipe order: " .. tostring(err)) end
end
local function setSortMode(mode)
if mode ~= "default" and mode ~= "cost" and mode ~= "profit" then
say("Use /pp sort default, /pp sort cost, or /pp sort profit.")
return
end
DB.sortMode = mode
if recipeSort.button then recipeSort.button:SetText("Sort: " .. SORT_LABELS[mode]) end
refreshSortedRecipes()
end
local function ensureRecipeSort()
local page = ProfessionsFrame and ProfessionsFrame.CraftingPage
local list = page and page.RecipeList
local scrollBox = list and list.ScrollBox
if not scrollBox or type(scrollBox.SetDataProvider) ~= "function" or type(CreateTreeDataProvider) ~= "function" then return end
if recipeSort.scrollBox ~= scrollBox then
local nativeSetDataProvider = scrollBox.SetDataProvider
scrollBox.SetDataProvider = function(self, provider, ...)
local sorted, displayProvider = pcall(sortedRecipeProvider, provider)
recipeSort.revision = cacheRevision
return nativeSetDataProvider(self, sorted and displayProvider or provider, ...)
end
recipeSort.scrollBox = scrollBox
if recipeSort.button then recipeSort.button:Hide() end
recipeSort.button = nil
end
if not recipeSort.button then
local button = CreateFrame("Button", nil, list, "UIPanelButtonTemplate")
button:SetSize(260, 22)
button:SetPoint("TOPLEFT", list, "BOTTOMLEFT", 6, -10)
button:SetFrameLevel((list:GetFrameLevel() or 1) + 5)
button:SetScript("OnClick", function()
local current = DB.sortMode or "default"
for index, mode in ipairs(SORT_MODES) do
if mode == current then
setSortMode(SORT_MODES[(index % #SORT_MODES) + 1])
return
end
end
setSortMode("default")
end)
button:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText("Profession Pricer Recipe Sort")
GameTooltip:AddLine("Click to cycle through default order, lowest AH craft cost, and highest estimated profit per craft.", 1, 1, 1, true)
GameTooltip:AddLine("Unlearned and unpriced recipes follow priced learned recipes.", .8, .8, .8, true)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
recipeSort.button = button
button:SetText("Sort: " .. SORT_LABELS[DB.sortMode or "default"])
if DB.sortMode ~= "default" then refreshSortedRecipes() end
end
end
local function hideCraft()
if craft.frame then craft.frame:Hide() end
for _, line in ipairs(craft.reagentLines) do line:Hide() end
craft.recipeID = nil
end
local function ensureCraftFrame(form)
if craft.owner == form and craft.frame then return end
hideCraft()
craft.owner = form
craft.reagentLines = {}
local frame = CreateFrame("Frame", nil, form)
frame:SetSize(190, 42)
frame:SetPoint("TOPRIGHT", form, "TOPRIGHT", -18, -48)
frame:SetFrameLevel((form:GetFrameLevel() or 1) + 30)
local cost = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
cost:SetPoint("TOPRIGHT", 0, 0)
cost:SetWidth(190)
cost:SetJustifyH("RIGHT")
local value = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
value:SetPoint("TOPRIGHT", cost, "BOTTOMRIGHT", 0, -2)
value:SetWidth(190)
value:SetJustifyH("RIGHT")
local margin = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
margin:SetPoint("TOPRIGHT", value, "BOTTOMRIGHT", 0, -2)
margin:SetWidth(190)
margin:SetJustifyH("RIGHT")
frame.cost, frame.value, frame.margin = cost, value, margin
craft.frame = frame
end
local function showReagentPrices(form, pricing)
for _, line in ipairs(craft.reagentLines) do line:Hide() end
for index, reagent in ipairs(pricing.reagents) do
local line = craft.reagentLines[index]
if not line then
line = form:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
craft.reagentLines[index] = line
end
line:ClearAllPoints()
line:SetPoint("TOPLEFT", form, "TOPLEFT", 190, -159 - ((index - 1) * 52))
line:SetWidth(145)
line:SetJustifyH("LEFT")
if reagent.price then
if reagent.quantity > 1 then
line:SetText("|cff33ff99" .. money(reagent.price) .. " ea / " .. money(reagent.price * reagent.quantity) .. " total|r")
else
line:SetText("|cff33ff99" .. money(reagent.price) .. " each|r")
end
else
line:SetText("|cff888888N/A|r")
end
line:Show()
end
end
updateCraftPriceUI = function()
local recipeID, form = selectedRecipe()
if not recipeID or not form or not form:IsShown() then hideCraft(); return end
local pricing = recipePricing(recipeID)
if not pricing then hideCraft(); return end
ensureCraftFrame(form)
craft.frame:Show()
craft.frame.cost:SetText(pricing.missing and "AH Craft Cost: |cffaaaaaaN/A|r" or "AH Craft Cost: " .. money(pricing.cost))
if pricing.outputValue then
craft.frame.value:SetText("AH Result Value: " .. money(pricing.outputValue))
if pricing.missing then
craft.frame.margin:SetText("")
else
local difference = pricing.outputValue - pricing.cost
if difference >= 0 then
craft.frame.margin:SetText("|cff33ff66Avg Profit / Craft: +" .. money(difference) .. "|r")
else
craft.frame.margin:SetText("|cffff5555Avg Loss / Craft: -" .. money(-difference) .. "|r")
end
end
else
craft.frame.value:SetText("AH Result Value: |cffaaaaaaN/A|r")
craft.frame.margin:SetText("")
end
showReagentPrices(form, pricing)
craft.recipeID = recipeID
craft.revision = cacheRevision
end
local function status()
say(comma(countPrices()) .. " cached item prices.")
if browse.active then
say("browse scan: " .. comma(countSeen()) .. " summaries across " .. comma(browse.pages) .. " result pages.")
elseif DB.lastBrowseScan then
say("last browse scan: " .. comma(DB.lastBrowseItems or 0) .. " summaries, " .. age(DB.lastBrowseScan) .. " ago.")
end
if replicate.active then
say(replicate.waiting and "full scan: waiting for snapshot." or ("full scan: " .. comma(replicate.index) .. "/" .. comma(replicate.total) .. " auctions processed."))
elseif DB.lastFullScan then
say("last full scan: " .. comma(DB.lastFullItems or 0) .. " items, " .. age(DB.lastFullScan) .. " ago.")
end
end
SLASH_PROFESSIONPRICER1 = "/pp"
SLASH_PROFESSIONPRICER2 = "/pricer"
SlashCmdList.PROFESSIONPRICER = function(raw)
local command = (raw or ""):lower():match("^%s*(.-)%s*$")
if command == "scan" then
startBrowseScan()
elseif command == "fullscan" then
startFullScan()
elseif command == "status" then
status()
elseif command:match("^sort%s+") then
setSortMode(command:match("^sort%s+(%S+)$"))
else
say("/pp scan - browse the Auction House for prices")
say("/pp fullscan - request a full AH snapshot when available")
say("/pp status - show cached prices and scan progress")
say("/pp sort default|cost|profit - order recipes across the profession")
end
end
app:SetScript("OnEvent", function(_, event, ...)
if event == "ADDON_LOADED" then
if ... ~= ADDON then return end
ProfessionPricerDB = ProfessionPricerDB or {}
DB = ProfessionPricerDB
DB.prices = DB.prices or {}
if not SORT_LABELS[DB.sortMode] then DB.sortMode = "default" end
ensureRecipeSort()
say("loaded. Use /pp help for commands.")
elseif event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_LIST_UPDATE" or event == "TRADE_SKILL_DATA_SOURCE_CHANGED" then
C_Timer.After(0.1, function() ensureRecipeSort(); updateCraftPriceUI() end)
elseif event == "AUCTION_HOUSE_SHOW" then
C_Timer.After(0.1, createScanButton)
elseif event == "AUCTION_HOUSE_CLOSED" then
if browse.active then endBrowse("Auction House closed", false) end
elseif event == "AUCTION_HOUSE_BROWSE_RESULTS_UPDATED" or event == "AUCTION_HOUSE_BROWSE_RESULTS_ADDED" then
browseResultsUpdated()
elseif event == "COMMODITY_SEARCH_RESULTS_UPDATED" then
local itemID = ...
local price = lowestCommodityPrice(itemID)
if price then savePrice(itemID, price, "commodity") end
elseif event == "ITEM_SEARCH_RESULTS_UPDATED" then
local itemKey = ...
if itemKey and itemKey.itemID then
local price = lowestItemPrice(itemKey)
if price then savePrice(itemKey.itemID, price, "item") end
end
elseif event == "REPLICATE_ITEM_LIST_UPDATE" then
replicationUpdated()
end
end)
local elapsedSinceCraftCheck = 0
app:SetScript("OnUpdate", function(_, elapsed)
if not DB then return end
elapsedSinceCraftCheck = elapsedSinceCraftCheck + elapsed
if elapsedSinceCraftCheck < 0.5 then return end
elapsedSinceCraftCheck = 0
ensureRecipeSort()
if DB.sortMode ~= "default" and recipeSort.scrollBox and recipeSort.revision ~= cacheRevision then
refreshSortedRecipes()
end
local recipeID, form = selectedRecipe()
if recipeID and form and form:IsShown() then
if recipeID ~= craft.recipeID or form ~= craft.owner or cacheRevision ~= craft.revision or not craft.frame or not craft.frame:IsShown() then
updateCraftPriceUI()
end
elseif craft.frame and craft.frame:IsShown() then
hideCraft()
end
end)
app:RegisterEvent("ADDON_LOADED")
app:RegisterEvent("AUCTION_HOUSE_SHOW")
app:RegisterEvent("AUCTION_HOUSE_CLOSED")
app:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_UPDATED")
app:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_ADDED")
app:RegisterEvent("COMMODITY_SEARCH_RESULTS_UPDATED")
app:RegisterEvent("ITEM_SEARCH_RESULTS_UPDATED")
app:RegisterEvent("REPLICATE_ITEM_LIST_UPDATE")
app:RegisterEvent("TRADE_SKILL_SHOW")
app:RegisterEvent("TRADE_SKILL_LIST_UPDATE")
app:RegisterEvent("TRADE_SKILL_DATA_SOURCE_CHANGED")