-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Scanner = TSM.Init("Service.AuctionScanClasses.Scanner") local Delay = TSM.Include("Util.Delay") local FSM = TSM.Include("Util.FSM") local Future = TSM.Include("Util.Future") local Log = TSM.Include("Util.Log") local ItemString = TSM.Include("Util.ItemString") local Event = TSM.Include("Util.Event") local ItemInfo = TSM.Include("Service.ItemInfo") local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper") local Util = TSM.Include("Service.AuctionScanClasses.Util") local ResultRow = TSM.Include("Service.AuctionScanClasses.ResultRow") local private = { resolveSellers = nil, pendingFuture = nil, query = nil, browseResults = nil, callback = nil, browseId = 1, browseIsNoScan = false, browseIndex = 1, browsePendingIndexes = {}, searchRow = nil, useCachedData = nil, retryCount = 0, requestFuture = Future.New("AUCTION_SCANNER_FUTURE"), requestResult = nil, fsm = nil, } -- ============================================================================ -- Module Loading -- ============================================================================ Scanner:OnModuleLoad(function() private.requestFuture:SetScript("OnCleanup", function() Delay.Cancel("AUCTION_SCANNER_DONE") private.fsm:ProcessEvent("EV_CANCEL") end) if TSM.IsWowClassic() then Event.Register("AUCTION_ITEM_LIST_UPDATE", function() private.fsm:SetLoggingEnabled(false) private.fsm:ProcessEvent("EV_BROWSE_RESULTS_UPDATED") private.fsm:SetLoggingEnabled(true) end) else Event.Register("COMMODITY_SEARCH_RESULTS_UPDATED", function() private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED") end) Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function() private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED") end) end private.fsm = FSM.New("AUCTION_SCANNER_FSM") :AddState(FSM.NewState("ST_INIT") :SetOnEnter(function() private.query = nil private.resolveSellers = nil private.useCachedData = nil private.searchRow = nil private.callback = nil private.retryCount = 0 Delay.Cancel("AUCTION_SCANNER_RETRY") if private.pendingFuture then private.pendingFuture:Cancel() private.pendingFuture = nil end end) :AddTransition("ST_BROWSE_SORT") :AddTransition("ST_BROWSE_CHECKING") :AddTransition("ST_SEARCH_GET_KEY") :AddEvent("EV_START_BROWSE", function(_, query, resolveSellers, browseResults, callback) assert(not private.query) private.query = query private.resolveSellers = resolveSellers private.browseResults = browseResults private.browseId = private.browseId + 1 private.browseIsNoScan = false private.callback = callback return "ST_BROWSE_SORT" end) :AddEvent("EV_START_BROWSE_NO_SCAN", function(_, query, itemKeys, browseResults, callback) assert(not TSM.IsWowClassic()) assert(not private.query) private.query = query private.browseResults = browseResults private.browseId = private.browseId + 1 private.browseIsNoScan = true private.callback = callback for _, itemKey in ipairs(itemKeys) do local baseItemString = ItemString.GetBaseFromItemKey(itemKey) private.ProcessBrowseResult(baseItemString, itemKey) end return "ST_BROWSE_CHECKING" end) :AddEvent("EV_START_SEARCH", function(_, query, resolveSellers, useCachedData, searchRow, callback) assert(not TSM.IsWowClassic()) assert(not private.query) private.query = query private.resolveSellers = resolveSellers private.useCachedData = useCachedData private.searchRow = searchRow private.callback = callback private.searchRow:SearchReset() return "ST_SEARCH_GET_KEY" end) ) :AddState(FSM.NewState("ST_BROWSE_SORT") :SetOnEnter(function() if not private.query:_SetSort() then Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler) return end return "ST_BROWSE_SEND" end) :AddTransition("ST_BROWSE_SORT") :AddTransition("ST_BROWSE_SEND") :AddTransition("ST_CANCELING") :AddEventTransition("EV_RETRY", "ST_BROWSE_SORT") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_BROWSE_SEND") :SetOnEnter(function() private.HandleAuctionHouseWrapperResult(private.query:_SendWowQuery()) end) :AddTransition("ST_BROWSE_SEND") :AddTransition("ST_BROWSE_CHECKING") :AddTransition("ST_CANCELING") :AddEvent("EV_FUTURE_SUCCESS", function() if TSM.IsWowClassic() then private.browseIndex = 1 wipe(private.browsePendingIndexes) else for _, result in ipairs(C_AuctionHouse.GetBrowseResults()) do local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey) private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity) end end return "ST_BROWSE_CHECKING" end) :AddEventTransition("EV_RETRY", "ST_BROWSE_SEND") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_BROWSE_CHECKING") :SetOnEnter(function() if not private.query:_BrowseIsPageValid() then -- this page isn't valid, so go to the next page return "ST_BROWSE_REQUEST_MORE" elseif not private.CheckBrowseResults() then -- result's aren't valid yet, so check again Delay.AfterFrame("AUCTION_SCANNER_RETRY", 1, private.RetryHandler) return end -- we're done with this set of browse results if private.callback then private.callback(private.query) end if private.browseIsNoScan or private.query:_BrowseIsDone() then -- we're done return "ST_BROWSE_DONE" else -- move on to the next page return "ST_BROWSE_REQUEST_MORE" end end) :AddTransition("ST_BROWSE_CHECKING") :AddTransition("ST_BROWSE_DONE") :AddTransition("ST_BROWSE_REQUEST_MORE") :AddTransition("ST_CANCELING") :AddEventTransition("EV_RETRY", "ST_BROWSE_CHECKING") :AddEventTransition("EV_BROWSE_RESULTS_UPDATED", "ST_BROWSE_CHECKING") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_BROWSE_REQUEST_MORE") :SetOnEnter(function(_, isRetry) if private.query:_BrowseIsDone(isRetry) then return "ST_BROWSE_CHECKING" else private.HandleAuctionHouseWrapperResult(private.query:_BrowseRequestMore(isRetry)) end end) :AddTransition("ST_BROWSE_REQUEST_MORE") :AddTransition("ST_BROWSE_CHECKING") :AddTransition("ST_CANCELING") :AddEvent("EV_FUTURE_SUCCESS", function(_, ...) if TSM.IsWowClassic() then private.browseIndex = 1 wipe(private.browsePendingIndexes) else local newResults = ... for _, result in ipairs(newResults) do local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey) private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity) end end return "ST_BROWSE_CHECKING" end) :AddEvent("EV_RETRY", function() return "ST_BROWSE_REQUEST_MORE", true end) :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_BROWSE_DONE") :SetOnEnter(function() private.HandleRequestDone(true) return "ST_INIT" end) :AddTransition("ST_INIT") ) :AddState(FSM.NewState("ST_SEARCH_GET_KEY") :SetOnEnter(function() assert(not TSM.IsWowClassic()) if not private.searchRow:SearchIsReady() then Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler) return end return "ST_SEARCH_SEND" end) :AddTransition("ST_SEARCH_GET_KEY") :AddTransition("ST_SEARCH_SEND") :AddTransition("ST_CANCELING") :AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_SEND") :AddEventTransition("EV_RETRY", "ST_SEARCH_GET_KEY") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_SEARCH_SEND") :SetOnEnter(function() assert(not TSM.IsWowClassic()) if not AuctionHouseWrapper.IsOpen() then return "ST_CANCELING" end if private.useCachedData and private.searchRow:HasCachedSearchData() then return "ST_SEARCH_REQUEST_MORE" end local future, delayTime = private.searchRow:SearchSend() if future then private.HandleAuctionHouseWrapperResult(future) else if not delayTime then Log.Err("Failed to send search query - retrying") delayTime = 0.5 end -- try again after a delay Delay.AfterTime("AUCTION_SCANNER_RETRY", delayTime, private.RetryHandler) end end) :AddTransition("ST_SEARCH_SEND") :AddTransition("ST_SEARCH_REQUEST_MORE") :AddTransition("ST_CANCELING") :AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_REQUEST_MORE") :AddEventTransition("EV_RETRY", "ST_SEARCH_SEND") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_SEARCH_REQUEST_MORE") :SetOnEnter(function() assert(not TSM.IsWowClassic()) local baseItemString = private.searchRow:GetBaseItemString() -- get if the item is a commodity or not local isCommodity = ItemInfo.IsCommodity(baseItemString) if isCommodity == nil then Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler) return end local isDone, future = private.searchRow:SearchCheckStatus() if isDone then return "ST_SEARCH_CHECKING" elseif future then private.HandleAuctionHouseWrapperResult(future) else Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler) end end) :AddTransition("ST_SEARCH_SEND") :AddTransition("ST_SEARCH_CHECKING") :AddTransition("ST_CANCELING") :AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_CHECKING") :AddEventTransition("EV_RETRY", "ST_SEARCH_SEND") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_SEARCH_CHECKING") :SetOnEnter(function() assert(not TSM.IsWowClassic()) Delay.Cancel("AUCTION_SCANNER_RETRY") private.searchRow:PopulateSubRows(private.browseId) -- check if all the sub rows have their data local isDone = true for _, subRow in private.searchRow:SubRowIterator(true) do if not subRow:HasRawData() or not subRow:HasItemString() then isDone = false elseif private.resolveSellers and not subRow:HasOwners() and not private.query:_IsFiltered(subRow, true) then -- waiting for owner info isDone = false end end if not isDone and private.retryCount >= 100 then -- out of retries, so give up return "ST_SEARCH_DONE", false elseif not isDone then -- we'll try again private.retryCount = private.retryCount + 1 Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler) return end -- filter the sub rows we don't care about private.searchRow:FilterSubRows(private.query) if private.callback then private.callback(private.query, private.searchRow) end if private.searchRow:SearchNext() then -- there is more to search return "ST_SEARCH_GET_KEY" else -- scanned everything return "ST_SEARCH_DONE", true end end) :AddTransition("ST_SEARCH_GET_KEY") :AddTransition("ST_SEARCH_CHECKING") :AddTransition("ST_SEARCH_DONE") :AddTransition("ST_CANCELING") :AddEventTransition("EV_RETRY", "ST_SEARCH_CHECKING") :AddEventTransition("EV_SEARCH_RESULTS_UPDATED", "ST_SEARCH_CHECKING") :AddEventTransition("EV_CANCEL", "ST_CANCELING") ) :AddState(FSM.NewState("ST_SEARCH_DONE") :SetOnEnter(function(_, result) assert(not TSM.IsWowClassic()) private.HandleRequestDone(result) return "ST_INIT" end) :AddTransition("ST_INIT") ) :AddState(FSM.NewState("ST_CANCELING") :SetOnEnter(function() Delay.Cancel("AUCTION_SCANNER_DONE") return "ST_INIT" end) :AddTransition("ST_INIT") ) :Init("ST_INIT", nil) end) -- ============================================================================ -- Module Functions -- ============================================================================ function Scanner.Browse(query, resolveSellers, results, callback) private.requestFuture:Start() private.fsm:ProcessEvent("EV_START_BROWSE", query, resolveSellers, results, callback) return private.requestFuture end function Scanner.BrowseNoScan(query, itemKeys, results, callback) assert(not TSM.IsWowClassic()) private.requestFuture:Start() private.fsm:ProcessEvent("EV_START_BROWSE_NO_SCAN", query, itemKeys, results, callback) return private.requestFuture end function Scanner.Search(query, resolveSellers, useCachedData, browseRow, callback) assert(not TSM.IsWowClassic()) private.requestFuture:Start() private.fsm:ProcessEvent("EV_START_SEARCH", query, resolveSellers, useCachedData, browseRow, callback) return private.requestFuture end function Scanner.Cancel() private.requestFuture:Done(false) end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.ProcessBrowseResult(baseItemString, ...) if private.browseResults[baseItemString] then private.browseResults[baseItemString]:Merge(...) else private.browseResults[baseItemString] = ResultRow.Get(private.query, ...) end return private.browseResults[baseItemString] end function private.PendingFutureDoneHandler() local result = private.pendingFuture:GetValue() private.pendingFuture = nil if result then private.fsm:ProcessEvent("EV_FUTURE_SUCCESS", result) else Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler) end end function private.RetryHandler() private.fsm:SetLoggingEnabled(false) private.fsm:ProcessEvent("EV_RETRY") private.fsm:SetLoggingEnabled(true) end function private.RequestDoneHandler() local result = private.requestResult private.requestResult = nil private.requestFuture:Done(result) end function private.HandleAuctionHouseWrapperResult(future) if future then private.pendingFuture = future private.pendingFuture:SetScript("OnDone", private.PendingFutureDoneHandler) else Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler) end end function private.HandleRequestDone(result) private.requestResult = result -- delay a bit so that we complete our current FSM transition Delay.AfterTime("AUCTION_SCANNER_DONE", 0, private.RequestDoneHandler) end function private.CheckBrowseResults() if TSM.IsWowClassic() then -- process as many auctions as we can local numAuctions = GetNumAuctionItems("list") for i = #private.browsePendingIndexes, 1, -1 do local index = private.browsePendingIndexes[i] if private.ProcessBrowseResultClassic(index) then tremove(private.browsePendingIndexes, i) end end local index = private.browseIndex while index <= numAuctions and #private.browsePendingIndexes < 50 do if not private.ProcessBrowseResultClassic(index) then tinsert(private.browsePendingIndexes, index) end index = index + 1 end private.browseIndex = index if private.browseIndex <= numAuctions or #private.browsePendingIndexes > 0 then return false end end -- check if there's data still pending local hasPendingData = false for _, row in pairs(private.browseResults) do if not row:PopulateBrowseData() then hasPendingData = true -- keep going so we issue requests for all pending rows end end if hasPendingData then return false end -- filter the results local numRemoved = 0 for baseItemString, row in pairs(private.browseResults) do -- filter the itemKeys we don't care about and rows which don't match the query if row:IsFiltered(private.query) then private.browseResults[baseItemString] = nil numRemoved = numRemoved + 1 end if TSM.IsWowClassic() then if row:FilterSubRows(private.query) then -- no more subRows, so filter the entire row private.browseResults[baseItemString] = nil numRemoved = numRemoved + 1 end end end if numRemoved > 0 then Log.Info("Removed %d results", numRemoved) end return true end function private.ProcessBrowseResultClassic(index) local rawName, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", index) local itemLink = GetAuctionItemLink("list", index) local baseItemString = ItemString.GetBase(itemLink) local timeLeft = GetAuctionItemTimeLeft("list", index) seller = Util.FixSellerName(seller, sellerFull) if not rawName or rawName == "" or not baseItemString or not buyout or not stackSize or not timeLeft or (not seller and private.resolveSellers) then return false end local row = private.ProcessBrowseResult(baseItemString, itemLink) -- amazingly, GetAuctionItemLink could return nil the next time it's called (within the same frame), so pass through our itemLink row:PopulateSubRows(private.browseId, index, itemLink) return true end