512 lines
17 KiB
Lua
512 lines
17 KiB
Lua
-- ------------------------------------------------------------------------------ --
|
|
-- 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
|