TradeSkillMaster/LibTSM/Service/AuctionScanClasses/Scanner.lua

512 lines
17 KiB
Lua
Raw Permalink Normal View History

2020-11-13 14:13:12 -05:00
-- ------------------------------------------------------------------------------ --
-- 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