-- ------------------------------------------------------------------------------ -- -- 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