710 lines
29 KiB
Lua
Executable File
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")
|