TradeSkillMaster/LibTSM/Service/AuctionScanClasses/ScanManager.lua

621 lines
18 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- This file contains code for scanning the auction house
local _, TSM = ...
local ScanManager = TSM.Init("Service.AuctionScanClasses.ScanManager")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Math = TSM.Include("Util.Math")
local ObjectPool = TSM.Include("Util.ObjectPool")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Query = TSM.Include("Service.AuctionScanClasses.Query")
local QueryUtil = TSM.Include("Service.AuctionScanClasses.QueryUtil")
local AuctionScanManager = TSM.Include("LibTSMClass").DefineClass("AuctionScanManager")
local private = {
objectPool = ObjectPool.New("AUCTION_SCAN_MANAGER", AuctionScanManager),
}
-- arbitrary estimate that finishing the browse request is worth 10% of the query's progress
local BROWSE_PROGRESS = 0.1
-- ============================================================================
-- Module Functions
-- ============================================================================
function ScanManager.Get()
return private.objectPool:Get()
end
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function AuctionScanManager.__init(self)
self._resolveSellers = nil
self._ignoreItemLevel = nil
self._queries = {}
self._queriesScanned = 0
self._queryDidBrowse = false
self._onProgressUpdateHandler = nil
self._onQueryDoneHandler = nil
self._resultsUpdateCallbacks = {}
self._nextSearchItemFunction = nil
self._currentSearchChangedCallback = nil
self._findResult = {}
self._cancelled = false
self._shouldPause = false
self._paused = false
self._scanQuery = nil
self._findQuery = nil
self._numItems = nil
self._queryCallback = function(query, searchRow)
for func in pairs(self._resultsUpdateCallbacks) do
func(self, query, searchRow)
end
end
end
function AuctionScanManager._Release(self)
self._resolveSellers = nil
self._ignoreItemLevel = nil
for _, query in ipairs(self._queries) do
query:Release()
end
wipe(self._queries)
self._queriesScanned = 0
self._queryDidBrowse = false
self._onProgressUpdateHandler = nil
self._onQueryDoneHandler = nil
wipe(self._resultsUpdateCallbacks)
self._nextSearchItemFunction = nil
self._currentSearchChangedCallback = nil
self._cancelled = false
self._shouldPause = false
self._paused = false
wipe(self._findResult)
self._scanQuery = nil
if self._findQuery then
self._findQuery:Release()
self._findQuery = nil
end
self._numItems = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AuctionScanManager.Release(self)
self:_Release()
private.objectPool:Recycle(self)
end
function AuctionScanManager.SetResolveSellers(self, resolveSellers)
self._resolveSellers = resolveSellers
return self
end
function AuctionScanManager.SetIgnoreItemLevel(self, ignoreItemLevel)
self._ignoreItemLevel = ignoreItemLevel
return self
end
function AuctionScanManager.SetScript(self, script, handler)
if script == "OnProgressUpdate" then
self._onProgressUpdateHandler = handler
elseif script == "OnQueryDone" then
self._onQueryDoneHandler = handler
elseif script == "OnCurrentSearchChanged" then
self._currentSearchChangedCallback = handler
else
error("Unknown AuctionScanManager script: "..tostring(script))
end
return self
end
function AuctionScanManager.AddResultsUpdateCallback(self, func)
self._resultsUpdateCallbacks[func] = true
end
function AuctionScanManager.RemoveResultsUpdateCallback(self, func)
self._resultsUpdateCallbacks[func] = nil
end
function AuctionScanManager.SetNextSearchItemFunction(self, func)
self._nextSearchItemFunction = func
end
function AuctionScanManager.GetNumQueries(self)
return #self._queries
end
function AuctionScanManager.QueryIterator(self, offset)
return private.QueryIteratorHelper, self._queries, offset or 0
end
function AuctionScanManager.NewQuery(self)
local query = Query.Get()
self:_AddQuery(query)
return query
end
function AuctionScanManager.AddItemListQueriesThreaded(self, itemList)
assert(Threading.IsThreadContext())
-- remove duplicates
local usedItems = TempTable.Acquire()
for i = #itemList, 1, -1 do
local itemString = itemList[i]
if usedItems[itemString] then
tremove(itemList, i)
end
usedItems[itemString] = true
end
TempTable.Release(usedItems)
self._numItems = #itemList
QueryUtil.GenerateThreaded(itemList, private.NewQueryCallback, self)
end
function AuctionScanManager.ScanQueriesThreaded(self)
assert(Threading.IsThreadContext())
self._queriesScanned = 0
self._cancelled = false
AuctionHouseWrapper.GetAndResetTotalHookedTime()
self:_NotifyProgressUpdate()
-- loop through each filter to perform
local allSuccess = true
while self._queriesScanned < #self._queries do
local query = self._queries[self._queriesScanned + 1]
-- run the browse query
local querySuccess, numNewResults = self:_ProcessQuery(query)
if not querySuccess then
allSuccess = false
break
end
self._queriesScanned = self._queriesScanned + 1
self:_NotifyProgressUpdate()
if self._onQueryDoneHandler then
self:_onQueryDoneHandler(query, numNewResults)
end
self:_Pause()
end
if allSuccess then
local hookedTime, topAddon, topTime = AuctionHouseWrapper.GetAndResetTotalHookedTime()
if hookedTime > 1 and topAddon ~= "Blizzard_AuctionHouseUI" then
Log.PrintfUser(L["Scan was slowed down by %s seconds by other AH addons (%s seconds by %s)."], Math.Round(hookedTime, 0.1), Math.Round(topTime, 0.1), topAddon)
end
end
return allSuccess
end
function AuctionScanManager.FindAuctionThreaded(self, findSubRow, noSeller)
assert(Threading.IsThreadContext())
wipe(self._findResult)
if TSM.IsWowClassic() then
return self:_FindAuctionThreaded(findSubRow, noSeller)
else
return self:_FindAuctionThreaded83(findSubRow, noSeller)
end
end
function AuctionScanManager.PrepareForBidOrBuyout(self, index, subRow, noSeller, quantity, itemBuyout)
if TSM.IsWowClassic() then
return subRow:EqualsIndex(index, noSeller)
else
local itemString = subRow:GetItemString()
if ItemInfo.IsCommodity(itemString) then
local future = AuctionHouseWrapper.StartCommoditiesPurchase(ItemString.ToId(itemString), quantity, itemBuyout)
if not future then
return false
end
return true, future
else
return true
end
end
end
function AuctionScanManager.PlaceBidOrBuyout(self, index, bidBuyout, subRow, quantity)
if TSM.IsWowClassic() then
PlaceAuctionBid("list", index, bidBuyout)
return true
else
local itemString = subRow:GetItemString()
local future = nil
if ItemInfo.IsCommodity(itemString) then
local itemId = ItemString.ToId(itemString)
future = AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity)
else
local _, auctionId = subRow:GetListingInfo()
future = AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout)
quantity = 1
end
if not future then
return false
end
-- TODO: return this future and record the buyout once the future is done
future:Cancel()
return true
end
end
function AuctionScanManager.GetProgress(self)
local numQueries = self:GetNumQueries()
if self._queriesScanned == numQueries then
return 1
end
local currentQuery = self._queries[self._queriesScanned + 1]
local searchProgress = nil
if not self._queryDidBrowse or TSM.IsWowClassic() then
searchProgress = 0
else
searchProgress = currentQuery:GetSearchProgress() * (1 - BROWSE_PROGRESS) + BROWSE_PROGRESS
end
local queryStep = 1 / numQueries
local progress = min((self._queriesScanned + searchProgress) * queryStep, 1)
return progress, self._paused
end
function AuctionScanManager.Cancel(self)
self._cancelled = true
if self._scanQuery then
self._scanQuery:CancelBrowseOrSearch()
self._scanQuery = nil
end
end
function AuctionScanManager.SetPaused(self, paused)
self._shouldPause = paused
if self._scanQuery then
self._scanQuery:CancelBrowseOrSearch()
self._scanQuery = nil
end
end
function AuctionScanManager.GetNumItems(self)
return self._numItems
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function AuctionScanManager._AddQuery(self, query)
query:SetResolveSellers(self._resolveSellers)
query:SetCallback(self._queryCallback)
tinsert(self._queries, query)
end
function AuctionScanManager._IsCancelled(self)
return self._cancelled
end
function AuctionScanManager._Pause(self)
if not self._shouldPause then
return
end
self._paused = true
self:_NotifyProgressUpdate()
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback()
end
while self._shouldPause do
Threading.Yield(true)
end
self._paused = false
self:_NotifyProgressUpdate()
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback()
end
end
function AuctionScanManager._NotifyProgressUpdate(self)
if self._onProgressUpdateHandler then
self:_onProgressUpdateHandler()
end
end
function AuctionScanManager._ProcessQuery(self, query)
local prevMaxBrowseId = 0
for _, row in query:BrowseResultsIterator() do
prevMaxBrowseId = max(prevMaxBrowseId, row:GetMinBrowseId())
end
-- run the browse query
self._queryDidBrowse = false
while not self:_DoBrowse(query) do
if self._shouldPause then
-- this browse failed due to a pause request, so try again after we're resumed
self:_Pause()
-- wipe the browse results since we're going to do another search
query:WipeBrowseResults()
else
return false, 0
end
end
self._queryDidBrowse = true
self:_NotifyProgressUpdate()
local numNewResults = 0
if TSM.IsWowClassic() then
for _, row in query:BrowseResultsIterator() do
if row:GetMinBrowseId() > prevMaxBrowseId then
numNewResults = numNewResults + row:GetNumSubRows()
end
end
return true, numNewResults
end
local rows = Threading.AcquireSafeTempTable()
for baseItemString, row in query:BrowseResultsIterator() do
rows[baseItemString] = row
end
while true do
local baseItemString, row = nil, nil
if self._nextSearchItemFunction then
baseItemString = self._nextSearchItemFunction()
row = baseItemString and rows[baseItemString]
end
if not row then
baseItemString, row = next(rows)
end
if not row then
break
end
rows[baseItemString] = nil
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback(baseItemString)
end
-- store all the existing auctionIds so we can see what changed
local prevAuctionIds = Threading.AcquireSafeTempTable()
for _, subRow in row:SubRowIterator() do
local _, auctionId = subRow:GetListingInfo()
assert(not prevAuctionIds[auctionId])
prevAuctionIds[auctionId] = true
end
-- send the query for this item
while not self:_DoSearch(query, row) do
if self._shouldPause then
-- this search failed due to a pause request, so try again after we're resumed
self:_Pause()
-- wipe the search results since we're going to do another search
row:WipeSearchResults()
else
Threading.ReleaseSafeTempTable(prevAuctionIds)
Threading.ReleaseSafeTempTable(rows)
return false, numNewResults
end
end
local numSubRows = row:GetNumSubRows()
for _, subRow in row:SubRowIterator() do
local _, auctionId = subRow:GetListingInfo()
if not prevAuctionIds[auctionId] then
numNewResults = numNewResults + 1
end
end
Threading.ReleaseSafeTempTable(prevAuctionIds)
if numSubRows == 0 then
-- remove this row since there are no search results
query:RemoveResultRow(row)
end
self:_NotifyProgressUpdate()
self:_Pause()
Threading.Yield()
end
Threading.ReleaseSafeTempTable(rows)
return true, numNewResults
end
function AuctionScanManager._DoBrowse(self, query, ...)
return self:_DoBrowseSearchHelper(query, query:Browse(...))
end
function AuctionScanManager._DoSearch(self, query, ...)
return self:_DoBrowseSearchHelper(query, query:Search(...))
end
function AuctionScanManager._DoBrowseSearchHelper(self, query, future)
if not future then
return false
end
self._scanQuery = query
local result = Threading.WaitForFuture(future)
self._scanQuery = nil
Threading.Yield()
return result
end
function AuctionScanManager._FindAuctionThreaded(self, row, noSeller)
self._cancelled = false
-- make sure we're not in the middle of a query where the results are going to change on us
Threading.WaitForFunction(CanSendAuctionQuery)
-- search the current page for the auction
if self:_FindAuctionOnCurrentPage(row, noSeller) then
Log.Info("Found on current page")
return self._findResult
end
-- search for the item
local page, maxPage = 0, nil
while true do
-- query the AH
if self._findQuery then
self._findQuery:Release()
end
local itemString = row:GetItemString()
local level = ItemInfo.GetMinLevel(itemString)
local quality = ItemInfo.GetQuality(itemString)
assert(level and quality)
self._findQuery = Query.Get()
:SetStr(ItemInfo.GetName(itemString), true)
:SetQualityRange(quality, quality)
:SetLevelRange(level, level)
:SetClass(ItemInfo.GetClassId(itemString), ItemInfo.GetSubClassId(itemString))
:SetItems(itemString)
:SetResolveSellers(not noSeller)
:SetPage(page)
local filterSuccess = self:_DoBrowse(self._findQuery)
if self._findQuery then
self._findQuery:Release()
self._findQuery = nil
end
if not filterSuccess then
break
end
-- search this page for the row
if self:_FindAuctionOnCurrentPage(row, noSeller) then
Log.Info("Found auction (%d)", page)
return self._findResult
elseif self:_IsCancelled() then
break
end
local numPages = ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE)
local canBeLater = private.FindAuctionCanBeOnLaterPage(row)
maxPage = maxPage or numPages - 1
if not canBeLater and page < maxPage then
maxPage = page
end
if canBeLater and page < maxPage then
Log.Info("Trying next page (%d)", page + 1)
page = page + 1
else
return
end
end
end
function AuctionScanManager._FindAuctionOnCurrentPage(self, subRow, noSeller)
local found = false
for i = 1, GetNumAuctionItems("list") do
if subRow:EqualsIndex(i, noSeller) then
tinsert(self._findResult, i)
found = true
end
end
return found
end
function AuctionScanManager._FindAuctionThreaded83(self, findSubRow, noSeller)
assert(findSubRow:IsSubRow())
self._cancelled = false
noSeller = noSeller or findSubRow:IsCommodity()
local row = findSubRow:GetResultRow()
local findHash, findHashNoSeller = findSubRow:GetHashes()
if not self:_DoSearch(row:GetQuery(), row, false) then
return nil
end
local result = nil
-- first try to find a subRow with a full matching hash
for _, subRow in row:SubRowIterator() do
local quantity, numAuctions = subRow:GetQuantities()
local hash = subRow:GetHashes()
if hash == findHash then
result = (result or 0) + quantity * numAuctions
end
end
if result then
return result
end
-- next try to find the first subRow with a matching no-seller hash
local firstHash = nil
for _, subRow in row:SubRowIterator() do
local quantity, numAuctions = subRow:GetQuantities()
local hash, hashNoSeller = subRow:GetHashes()
if (not firstHash or hash == firstHash) and hashNoSeller == findHashNoSeller then
firstHash = hash
result = (result or 0) + quantity * numAuctions
end
end
return result
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.NewQueryCallback(query, self)
self:_AddQuery(query)
end
function private.FindAuctionCanBeOnLaterPage(row)
local pageAuctions = GetNumAuctionItems("list")
if pageAuctions == 0 then
-- there are no auctions on this page, so it cannot be on a later one
return false
end
local _, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", pageAuctions)
local itemBuyout = (buyout > 0) and floor(buyout / stackSize) or 0
local _, rowItemBuyout = row:GetBuyouts()
if rowItemBuyout > itemBuyout then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowItemBuyout < itemBuyout then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
local rowStackSize = row:GetQuantities()
if rowStackSize > stackSize then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowStackSize < stackSize then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
seller = private.FixSellerName(seller, sellerFull) or "?"
local rowSeller = row:GetOwnerInfo()
if rowSeller > seller then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowSeller < seller then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
-- all the things we are sorting on are the same, so the auction could be on a later page
return true
end
function private.FixSellerName(seller, sellerFull)
local realm = GetRealmName()
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
return sellerFull
else
return seller
end
end
function private.QueryIteratorHelper(tbl, index)
index = index + 1
if index > #tbl then
return
end
return index, tbl[index]
end