Files
Xan-Asc 42dbe9d526 Enchanting fixes, reverted Crafting module to previous version that s… (#1)
* Enchanting fixes, reverted Crafting module to previous version that supports Ascension specific crafts and Vellums.

* Updated enchanting names from DB

* Added the remaining Ascension specific recipes

* Replace SpellNames2IDs.lua with UnitCastingSpellID

* Bugfix: Ascension uses exact quality QueryAuctionItems, TSM was designed for quality or higher

* Bugfix: Properly get list of Professions via index

* Replaced factionrealm with realm
Ascension does not have strict faction seperation

* GetTradeSkillCooldown -> SpellHasBaseCooldown
GetTradeSkillCooldown only tells you if a spell is on CD, not if it has a CD
Data is from DB, which isn't populated fully yet.

* Implement backported API GetSpellBaseCooldown

* bugfix: hasCD is nil if no cd, not 0
2023-04-07 03:55:47 -07:00

628 lines
22 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- http://www.curse.com/addons/wow/tradeskill-master --
-- --
-- A TradeSkillMaster Addon (http://tradeskillmaster.com) --
-- All Rights Reserved* - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- This file contains code for scanning the auction house
local TSM = select(2, ...)
local AuctionScanning = TSM:NewModule("AuctionScanning", "AceEvent-3.0")
TSMAPI.AuctionScan = {}
local RETRY_DELAY = 2
local MAX_RETRIES = 4
local BASE_DELAY = 0.10 -- time to delay for before trying to scan a page again when it isn't fully loaded
local private = { callbackHandler = nil, query = {}, options = {}, data = {}, isScanning = nil }
TSMAPI:RegisterForTracing(private, "TradeSkillMaster.AuctionScanning_private")
local scanCache = {}
local CACHE_DECAY_PER_DAY = 5
local CACHE_AUTO_HIT_TIME = 10 * 60
local SECONDS_PER_DAY = 60 * 60 * 24
local function DoCallback(...)
if type(private.callbackHandler) == "function" then
private.callbackHandler(...)
end
end
local function eventHandler(event)
if event == "AUCTION_HOUSE_CLOSED" then
-- auction house was closed, make sure all scanning is stopped
AuctionScanning:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
private.auctionHouseShown = false
DoCallback("INTERRUPTED")
private:StopScanning()
elseif event == "AUCTION_ITEM_LIST_UPDATE" then
-- gets called whenever the AH window is updated (something is shown in the results section)
AuctionScanning:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
TSMAPI:CancelFrame("updateDelay")
-- now that our query was successful, we can get our data
private:ScanAuctions()
end
end
function AuctionScanning:OnEnable()
AuctionScanning:RegisterEvent("AUCTION_HOUSE_CLOSED", eventHandler)
end
function private:ScanAuctionPage(resolveSellers)
local shown = GetNumAuctionItems("list")
local badData = false
local auctions = {}
for i = 1, shown do
-- checks to make sure all the data has been sent to the client
-- if not, the data is bad and we'll wait / try again
-- local count, _, _, _, _, _, _, buyout, _, _, _, seller = select(3, GetAuctionItemInfo("list", i))
local count, _, _, _, _, _, buyout, _, _, seller = select(3, GetAuctionItemInfo("list", i))
local itemString = TSMAPI:GetItemString(GetAuctionItemLink("list", i))
auctions[i] = { itemString = itemString, index = i, count = count, buyout = buyout, seller = seller }
if not (itemString and buyout and count and (seller or not resolveSellers or buyout == 0)) then
badData = true
end
end
return badData, auctions
end
function IsDuplicatePage()
if not private.pageTemp or GetNumAuctionItems("list") == 0 then return false end
local numLinks, prevLink = 0, nil
for i = 1, GetNumAuctionItems("list") do
-- local _, _, count, _, _, _, _, minBid, minInc, buyout, bid, _, _, seller = GetAuctionItemInfo("list", i)
local _, _, count, _, _, _, minBid, minInc, buyout, bid, _, seller = GetAuctionItemInfo("list", i)
local link = GetAuctionItemLink("list", i)
local temp = private.pageTemp[i]
if not prevLink then
prevLink = link
elseif prevLink ~= link then
prevLink = link
numLinks = numLinks + 1
end
if not temp or temp.count ~= count or temp.minBid ~= minBid or temp.minInc ~= minInc or temp.buyout ~= buyout or temp.bid ~= bid or temp.seller ~= seller or temp.link ~= link then
return false
end
end
if numLinks > 1 and private.pageTemp.shown == GetNumAuctionItems("list") then
return false
end
return true
end
local function PopulatePageTemp()
local shown = GetNumAuctionItems("list")
private.pageTemp = { numShown = shown }
for i = 1, shown do
-- checks to make sure all the data has been sent to the client
-- if not, the data is bad and we'll wait / try again
-- local _, _, count, _, _, _, _, minBid, minInc, buyout, bid, _, seller = GetAuctionItemInfo("list", i)
local _, _, count, _, _, _, minBid, minInc, buyout, bid, _, seller = GetAuctionItemInfo("list", i)
local link = GetAuctionItemLink("list", i)
private.pageTemp[i] = { count = count, minBid = minBid, minInc = minInc, buyout = buyout, bid = bid, seller = seller, link = link }
end
end
-- Starts a scan of the auction house.
-- query - A single query containing QueryAuctionItem paramters:
-- name, minLevel, maxLevel, invType, class, subClass, usable, quality
-- resolveSellers - whether or not to resolve seller names
-- maxPrice - stop scanning when prices go above this price
function TSMAPI.AuctionScan:RunQuery(query, callbackHandler, resolveSellers, maxPrice, doCache)
TSMAPI.AuctionScan:StopScan() -- stop any scan in progress
if not AuctionFrame:IsVisible() then
return -1 -- the auction house isn't open (return code -1)
elseif type(query) ~= "table" then
return -2 -- the scan queue is not a table (return code -2)
elseif not CanSendAuctionQuery() then
TSMAPI:CreateTimeDelay("cantSendAuctionQueryDelay", 0.1, function() TSMAPI.AuctionScan:RunQuery(query, callbackHandler, resolveSellers, maxPrice, doCache) end)
return 0 -- the query will start as soon as it can but did not start immediately (return code 0)
end
-- sort by buyout
SortAuctionItems("list", "buyout")
if IsAuctionSortReversed("list", "buyout") then
SortAuctionItems("list", "buyout")
end
-- setup the query
private.query = CopyTable(query)
private.query.page = 0 -- the current page of this query we're scanning
private.query.timeDelay = 0 -- a delay used to wait for information to show up
private.query.retries = 0 -- how many times we've done a hard retry so far
private.query.hardRetry = nil -- if a page hasn't loaded after we've tried a delay, we'll do a hard retry and re-send the query
private.cache = doCache and { query = CopyTable(query), items = {} } or nil
-- setup other stuff
wipe(private.data)
private.isScanning = true
private.callbackHandler = callbackHandler
private.resolveSellers = resolveSellers
private.scanType = "query"
private.maxPrice = maxPrice or math.huge
--starts scanning
private:SendQuery()
return 1 -- scan started successfully (return code 1)
end
function TSMAPI.AuctionScan:ScanLastPage(callbackHandler)
private:StopScanning() -- stop any scan in progress
if not AuctionFrame:IsVisible() then
return -1 -- the auction house isn't open (return code -1)
elseif not CanSendAuctionQuery() then
TSMAPI:CreateTimeDelay("cantSendAuctionQueryDelay", 0.1, function() TSMAPI.AuctionScan:ScanLastPage(callbackHandler) end)
return 0 -- the query will start as soon as it can but did not start immediately (return code 0)
end
-- clear the auction sort
SortAuctionClearSort("list")
-- setup the query
private.query = {name="", page=0}
private.query.timeDelay = 0 -- a delay used to wait for information to show up
private.query.retries = 0 -- how many times we've done a hard retry so far
private.query.hardRetry = nil -- if a page hasn't loaded after we've tried a delay, we'll do a hard retry and re-send the query
-- setup other stuff
wipe(private.data)
private.isScanning = true
private.callbackHandler = callbackHandler
private.scanType = "lastPage"
--starts scanning
private:SendQuery()
return 1 -- scan started successfully (return code 1)
end
-- sends a query to the AH frame once it is ready to be queried (uses frame as a delay)
function private:SendQuery()
if not private.isScanning then return end
if CanSendAuctionQuery() then
-- stop delay timer
TSMAPI:CancelFrame("queryDelay")
-- Query the auction house (then waits for AUCTION_ITEM_LIST_UPDATE to fire)
AuctionScanning:RegisterEvent("AUCTION_ITEM_LIST_UPDATE", eventHandler)
-- [exact] cardinal ruby 0 0 nil 0 0 0 0 0
-- [normal] cardinal ruby nil nil nil nil nil 0 nil nil
QueryAuctionItems(private.query.name, private.query.minLevel, private.query.maxLevel, private.query.invType, private.query.class, private.query.subClass, private.query.page, private.query.usable, private.query.quality)
else
-- run delay timer then try again to scan
TSMAPI:CreateTimeDelay("queryDelay", 0.05, private.SendQuery)
end
end
--scans the currently shown page of auctions and collects all the data
function private:ScanAuctions()
if not private.isScanning then return end
local shown, total = GetNumAuctionItems("list")
local totalPages = ceil(total / NUM_AUCTION_ITEMS_PER_PAGE)
if private.scanType == "numPages" then
local cacheData = TSM.db.realm.numPagesCache[private.query.cacheKey]
cacheData.lastScan = time()
local confidence = (120 - cacheData.confidence) / (CACHE_DECAY_PER_DAY * 2)
local diff = abs(cacheData.avg - totalPages)
if diff <= 1 and diff > 0.5 then
confidence = floor(confidence * (1.5 - diff))
elseif diff > 1 then
confidence = floor(confidence - CACHE_DECAY_PER_DAY * diff)
end
cacheData.confidence = max(floor(cacheData.confidence + confidence), 0)
cacheData.avg = (cacheData.avg * cacheData.numScans + totalPages) / (cacheData.numScans + 1)
cacheData.numScans = cacheData.numScans + 1
private:StopScanning()
return DoCallback("NUM_PAGES", totalPages)
elseif private.scanType == "lastPage" then
local lastPage = floor(total / NUM_AUCTION_ITEMS_PER_PAGE)
if private.query.page ~= lastPage then
private.query.page = lastPage
return private:SendQuery()
end
end
local dataIsBad, auctions = private:ScanAuctionPage(private.resolveSellers)
-- check that we have good data
if dataIsBad or IsDuplicatePage() then
if private.query.retries < MAX_RETRIES then
if private.query.hardRetry then
-- Hard retry
-- re-sends the entire query
private.query.retries = private.query.retries + 1
private.query.timeDelay = 0
private.query.hardRetry = nil
private:SendQuery()
else
-- Soft retry
-- runs a delay and then tries to scan the query again
private.query.timeDelay = private.query.timeDelay + BASE_DELAY
TSMAPI:CreateTimeDelay("updateDelay", BASE_DELAY, private.ScanAuctions)
-- If after 2 seconds of retrying we still don't have data, will go and requery to try and solve the issue
-- if we still don't have data, we try to scan it anyway and move on.
if private.query.timeDelay >= RETRY_DELAY then
private.query.hardRetry = true
end
end
return
end
end
if private.cache then
-- store info in cache
for i, v in ipairs(auctions) do
local cacheTmp = CopyTable(v)
cacheTmp.index = private.query.page * 50 + i
tinsert(private.cache, cacheTmp)
private.cache.items[cacheTmp.itemString] = true
end
end
private.query.hardRetry = nil
private.query.retries = 0
private.query.timeDelay = 0
if private.scanType ~= "lastPage" then
private.query.page = private.query.page + 1 -- increment current page
if totalPages > 0 then
DoCallback("SCAN_PAGE_UPDATE", private.query.page, totalPages)
end
end
PopulatePageTemp()
-- now that we know our query is good, time to verify and then store our data
for _, v in ipairs(auctions) do
if private:AddAuctionRecord(v.index) then
-- we've hit the max price so we're done scanning
private:StopScanning()
return DoCallback("SCAN_COMPLETE", private.data)
end
end
if private.scanType == "lastPage" then
return DoCallback("SCAN_LAST_PAGE_COMPLETE", private.data)
elseif private.query.page >= totalPages then
-- we have finished scanning this query
private:StopScanning()
return DoCallback("SCAN_COMPLETE", private.data)
end
-- query the next page and continue scanning
private:SendQuery()
end
-- Add a new record to the private.data table
function private:AddAuctionRecord(index)
-- local name, texture, count, _, _, _, _, minBid, minIncrement, buyout, bid, highBidder, highBidder_full, seller, seller_full = GetAuctionItemInfo("list", index)
local name, texture, count, _, _, _, minBid, minIncrement, buyout, bid, highBidder, seller = GetAuctionItemInfo("list", index)
seller = TSM:GetAuctionPlayer(seller, null)
highBidder = TSM:GetAuctionPlayer(highBidder, null)
local timeLeft = GetAuctionItemTimeLeft("list", index)
local link = GetAuctionItemLink("list", index)
local itemString = TSMAPI:GetItemString(link)
if not itemString then return end
-- Create a new entry in the table
if not private.data[itemString] then
private.data[itemString] = TSMAPI.AuctionScan:NewAuctionItem()
private.data[itemString]:SetItemLink(link)
private.data[itemString]:SetTexture(texture)
end
private.data[itemString]:AddAuctionRecord(count, minBid, minIncrement, buyout, bid, highBidder, seller or "?", timeLeft)
-- add the base item if necessary
local baseItemString = TSMAPI:GetBaseItemString(itemString)
if baseItemString ~= itemString then
-- Create a new entry in the table
if not private.data[baseItemString] then
private.data[baseItemString] = TSMAPI.AuctionScan:NewAuctionItem()
private.data[baseItemString]:SetItemLink(link)
private.data[baseItemString]:SetTexture(texture)
end
private.data[baseItemString]:AddAuctionRecord(count, minBid, minIncrement, buyout, bid, highBidder, seller or "?", timeLeft)
private.data[baseItemString].isBaseItem = true
end
if select(8, TSMAPI:GetSafeItemInfo(link)) == count then
return (buyout or 0) / count > (private.maxPrice or math.huge)
end
end
-- stops the scan when we are finished scanning, it was interrupted, or somebody stopped it
function private:StopScanning()
TSMAPI:CancelFrame("cantSendAuctionQueryDelay")
if not private.isScanning then return end
if private.cache then
-- store the cache info
sort(private.cache, function(a, b) return a.index < b.index end)
for itemString in pairs(private.cache.items) do
scanCache[itemString] = private.cache
end
wipe(private.cache.items)
private.cache = nil
end
-- cancel any delays that might still be running
TSMAPI:CancelFrame("queryDelay")
TSMAPI:CancelFrame("updateDelay")
AuctionScanning:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
private.isScanning = nil
private.pageTemp = nil
end
-- API for stopping the scan
-- returns true/false if we were/weren't actually scanning
function TSMAPI.AuctionScan:StopScan()
private:StopScanning()
TSM:StopGeneratingQueries()
end
-- Gets the number of pages for a given query
function TSMAPI.AuctionScan:GetNumPages(query, callbackHandler)
private:StopScanning() -- stop any scan in progress
if not AuctionFrame:IsVisible() then
return -1 -- the auction house isn't open (return code -1)
elseif type(query) ~= "table" then
return -2 -- the scan queue is not a table (return code -2)
elseif not CanSendAuctionQuery() then
TSMAPI:CreateTimeDelay("cantSendAuctionQueryDelay", 0.1, function() TSMAPI.AuctionScan:GetNumPages(query, callbackHandler) end)
return 0 -- the query will start as soon as it can but did not start immediately (return code 0)
end
-- fancy caching
local temp = {}
for i, field in ipairs({ "name", "minLevel", "maxLevel", "invType", "class", "subClass", "usable", "quality" }) do
temp[i] = tostring(query[field])
end
local cacheKey = table.concat(temp, "~")
local cacheData = TSM.db.realm.numPagesCache[cacheKey]
if cacheData then
local cacheHit
if time() - cacheData.lastScan < CACHE_AUTO_HIT_TIME then
-- auto cache hit
cacheHit = true
elseif random(1, 100) <= cacheData.confidence then
-- cache hit
cacheData.confidence = cacheData.confidence - floor(((time() - cacheData.lastScan) / SECONDS_PER_DAY) * CACHE_DECAY_PER_DAY + 0.5)
cacheData.confidence = max(cacheData.confidence, 0) -- ensure >= 0
cacheHit = true
end
if cacheHit then
local numPages = max(ceil(cacheData.avg), 1) -- round avg num of pages up and ensure >= 1
TSMAPI:CreateTimeDelay("numPagesCacheDelay", 0, function() callbackHandler("NUM_PAGES", numPages) end)
return 2
end
else
TSM.db.realm.numPagesCache[cacheKey] = { avg = 0, confidence = 0, numScans = 0, lastScan = 0 }
end
-- setup the query
private.query = CopyTable(query)
private.query.cacheKey = cacheKey
-- setup other stuff
wipe(private.data)
private.isScanning = true
private.callbackHandler = callbackHandler
private.scanType = "numPages"
--starts scanning
private:SendQuery()
return 1 -- scan started successfully (return code 1)
end
function TSMAPI.AuctionScan:CacheRemove(itemString, index)
if scanCache[itemString] then
tremove(scanCache[itemString], index)
end
end
function TSMAPI.AuctionScan:ClearCache()
wipe(scanCache)
end
local findPrivate = {}
findPrivate.findFrame = findPrivate.findFrame or CreateFrame("Frame")
local function eventHandler(frame, event)
if event == "AUCTION_HOUSE_SHOW" then
-- auction house was opened
elseif event == "AUCTION_HOUSE_CLOSED" then
frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
if findPrivate.isScanning then -- stop scanning if we were scanning (pass true to specify it was interrupted)
TSMAPI.AuctionScan:StopFindScan()
end
elseif event == "AUCTION_ITEM_LIST_UPDATE" then
frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
if findPrivate.isScanning then
findPrivate.timeDelay = 0
TSMAPI:CancelFrame("auctionFindScanDelay")
-- now that our query was successful we can get our data
findPrivate:ScanAuctions()
end
end
end
findPrivate.findFrame:SetScript("OnEvent", eventHandler)
findPrivate.findFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
findPrivate.findFrame:RegisterEvent("AUCTION_HOUSE_SHOW")
local function CompareTableKeys(tbl1, tbl2)
for _, key in ipairs(findPrivate.keys) do
if tbl1[key] ~= tbl2[key] then
return
end
end
return true
end
local function IsTargetAuction(index)
local itemString = TSMAPI:GetItemString(GetAuctionItemLink("list", index))
-- local _, _, count, _, _, _, _, minBid, bidIncrement, buyout, bidAmount, _, _, seller, seller_full = GetAuctionItemInfo("list", index)
local _, _, count, _, _, _, minBid, bidIncrement, buyout, bidAmount, _, _, seller = GetAuctionItemInfo("list", index)
seller = TSM:GetAuctionPlayer(seller, nil)
local bid = bidAmount == 0 and minBid or bidAmount
local tmp = { itemString = itemString, count = count, bid = bid, buyout = buyout, seller = seller }
return CompareTableKeys(tmp, findPrivate.targetInfo)
end
-- valid targetInfo keys: itemString, count, bid, buyout, seller
function TSMAPI.AuctionScan:FindAuction(callback, targetInfo, useCache)
if findPrivate.isScanning then TSMAPI.AuctionScan:StopFindScan() end
findPrivate.keys = { "itemString", "count", "bid", "buyout", "seller" }
for i = #findPrivate.keys, 1, -1 do
if not targetInfo[findPrivate.keys[i]] then
tremove(findPrivate.keys, i)
end
end
local cacheIndex
if useCache and scanCache[targetInfo.itemString] then
for i, v in ipairs(scanCache[targetInfo.itemString]) do
if CompareTableKeys(v, targetInfo) then
cacheIndex = i
break
end
end
end
if cacheIndex then
findPrivate.page = floor((cacheIndex - 1) / 50)
findPrivate.query = scanCache[targetInfo.itemString].query
else
local name, _, rarity, _, minLevel, class, subClass = TSMAPI:GetSafeItemInfo(targetInfo.itemString)
findPrivate.query = { name = name, minLevel = minLevel, maxLevel = minLevel, class = class, subClass = subClass, rarity = rarity }
findPrivate.page = 0
end
findPrivate.targetInfo = targetInfo
findPrivate.callback = callback
findPrivate.cacheIndex = cacheIndex
findPrivate.isScanning = targetInfo.itemString
findPrivate.retries = 0
findPrivate.hardRetry = nil
-- check if the item is on the current page
for i = 1, GetNumAuctionItems("list") do
if IsTargetAuction(i) then
TSMAPI.AuctionScan:StopFindScan()
TSMAPI:CreateTimeDelay("queryFoundDelay", 0.1, function() findPrivate.callback(i) end)
return
end
end
findPrivate:SendQuery()
end
-- sends a query to the AH frame once it is ready to be queried (uses frame as a delay)
function findPrivate:SendQuery()
if not findPrivate.isScanning then return end
if CanSendAuctionQuery() then
-- stop delay timer
TSMAPI:CancelFrame("auctionFindQueryDelay")
-- query the auction house (then waits for AUCTION_ITEM_LIST_UPDATE to fire)
findPrivate.findFrame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
local q = findPrivate.query
QueryAuctionItems(q.name, q.minLevel, q.maxLevel, q.invType, q.class, q.subClass, findPrivate.page, 0, q.rarity)
else
-- run delay timer then try again to scan
TSMAPI:CreateTimeDelay("auctionFindQueryDelay", 0.05, function() findPrivate:SendQuery() end)
end
end
-- scans the currently shown page of auctions and collects all the data
function findPrivate:ScanAuctions()
if not findPrivate.isScanning then return end
-- collects data on the query:
-- # of auctions on current page
-- # of pages total
local shown, total = GetNumAuctionItems("list")
local totalPages = math.ceil(total / 50)
local dataIsBad, temp = private:ScanAuctionPage(findPrivate.targetInfo.seller)
-- Check for bad data
if findPrivate.retries < 3 then
if dataIsBad then
if findPrivate.hardRetry then
-- Hard retry
-- re-sends the entire query
findPrivate.retries = findPrivate.retries + 1
findPrivate:SendQuery()
else
-- Soft retry
-- runs a delay and then tries to scan the query again
findPrivate.timeDelay = findPrivate.timeDelay + BASE_DELAY
TSMAPI:CreateTimeDelay("auctionFindScanDelay", BASE_DELAY, findPrivate.ScanAuctions)
-- If after 4 seconds of retrying we still don't have data, will go and requery to try and solve the issue
-- if we still don't have data, we try to scan it anyway and move on.
if findPrivate.timeDelay >= 4 then
findPrivate.hardRetry = true
findPrivate.retries = 0
end
end
return
end
end
findPrivate.hardRetry = nil
findPrivate.retries = 0
-- now that we know our query is good, time to verify and then store our data
for i = 1, shown do
if IsTargetAuction(temp[i].index) then
TSMAPI.AuctionScan:StopFindScan()
return findPrivate.callback(temp[i].index, findPrivate.cacheIndex == findPrivate.page and findPrivate.page * 50 + temp[i].index)
end
end
-- This query has more pages to scan
-- increment the page # and send the new query
if not findPrivate.cacheIndex and totalPages > (findPrivate.page + 1) then
findPrivate.page = findPrivate.page + 1
findPrivate:SendQuery()
return
end
-- we are done scanning!
TSMAPI.AuctionScan:StopFindScan()
return findPrivate.callback()
end
-- returns whether or not we're currently doing a find scan
function TSMAPI.AuctionScan:IsFindScanning()
return findPrivate.isScanning
end
-- stops the scan because it was either interrupted or it was completed successfully
function TSMAPI.AuctionScan:StopFindScan()
findPrivate.findFrame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
findPrivate.isScanning = nil
TSMAPI:CancelFrame("auctionFindQueryDelay")
TSMAPI:CancelFrame("auctionFindScanDelay")
end