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")