-- ------------------------------------------------------------------------------ --
-- 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 : _FindAuctionThreadedClassic ( findSubRow , noSeller )
else
return self : _FindAuctionThreadedRetail ( findSubRow )
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 . _FindAuctionThreadedClassic ( 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 = AuctionHouseWrapper.GetNumPages ( )
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 , AuctionHouseWrapper.GetNumAuctions ( ) do
if subRow : EqualsIndex ( i , noSeller ) then
tinsert ( self._findResult , i )
found = true
end
end
return found
end
function AuctionScanManager . _FindAuctionThreadedRetail ( self , findSubRow )
assert ( findSubRow : IsSubRow ( ) )
self._cancelled = false
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 = AuctionHouseWrapper.GetNumAuctions ( )
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