You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

617 lines
18 KiB

3 years ago
-- ------------------------------------------------------------------------------ --
-- 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
return AuctionHouseWrapper.PlaceBid(index, bidBuyout)
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
return future
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
if TSM.IsWowClassic() then
for _, row in query:BrowseResultsIterator() do
prevMaxBrowseId = max(prevMaxBrowseId, row:GetMinBrowseId())
end
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