TradeSkillMaster/LibTSM/Service/AuctionScanClasses/Query.lua

676 lines
21 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- AuctionQuery Class.
-- A class which is used to build a query to scan the auciton house.
-- @classmod AuctionQuery
local _, TSM = ...
local Query = TSM.Init("Service.AuctionScanClasses.Query")
local String = TSM.Include("Util.String")
local ObjectPool = TSM.Include("Util.ObjectPool")
local ItemString = TSM.Include("Util.ItemString")
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local Scanner = TSM.Include("Service.AuctionScanClasses.Scanner")
local LibTSMClass = TSM.Include("LibTSMClass")
local AuctionQuery = LibTSMClass.DefineClass("AuctionQuery")
local private = {
objectPool = ObjectPool.New("AUCTION_SCAN_QUERY", AuctionQuery),
}
local ITEM_SPECIFIC = newproxy()
local ITEM_BASE = newproxy()
local DEFAULT_SORTS = TSM.IsWowClassic() and
{ -- classic
"seller",
"quantity",
"unitprice",
} or
{ -- retail
{ sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false },
{ sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false },
}
local EMPTY_SORTS = {}
local INV_TYPES = {
CHEST = TSM.IsShadowlands() and Enum.InventoryType.IndexChestType or LE_INVENTORY_TYPE_CHEST_TYPE,
ROBE = TSM.IsShadowlands() and Enum.InventoryType.IndexRobeType or LE_INVENTORY_TYPE_ROBE_TYPE,
NECK = TSM.IsShadowlands() and Enum.InventoryType.IndexNeckType or LE_INVENTORY_TYPE_NECK_TYPE,
FINGER = TSM.IsShadowlands() and Enum.InventoryType.IndexFingerType or LE_INVENTORY_TYPE_FINGER_TYPE,
TRINKET = TSM.IsShadowlands() and Enum.InventoryType.IndexTrinketType or LE_INVENTORY_TYPE_TRINKET_TYPE,
HOLDABLE = TSM.IsShadowlands() and Enum.InventoryType.IndexHoldableType or LE_INVENTORY_TYPE_HOLDABLE_TYPE,
BODY = TSM.IsShadowlands() and Enum.InventoryType.IndexBodyType or LE_INVENTORY_TYPE_BODY_TYPE,
CLOAK = TSM.IsShadowlands() and Enum.InventoryType.IndexCloakType or LE_INVENTORY_TYPE_CLOAK_TYPE,
}
assert(Table.Count(INV_TYPES) == 8)
-- ============================================================================
-- Module Functions
-- ============================================================================
function Query.Get()
return private.objectPool:Get()
end
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function AuctionQuery.__init(self)
self._str = ""
self._strLower = ""
self._strMatch = ""
self._exact = false
self._minQuality = -math.huge
self._maxQuality = math.huge
self._minLevel = -math.huge
self._maxLevel = math.huge
self._minItemLevel = -math.huge
self._maxItemLevel = math.huge
self._class = nil
self._subClass = nil
self._invType = nil
self._classFilter1 = {}
self._classFilter2 = {}
self._usable = false
self._uncollected = false
self._upgrades = false
self._unlearned = false
self._canLearn = false
self._minPrice = 0
self._maxPrice = math.huge
self._items = {}
self._customFilters = {}
self._isBrowseDoneFunc = nil
self._specifiedPage = nil
self._getAll = nil
self._resolveSellers = false
self._callback = nil
self._queryTemp = {}
self._filtersTemp = {}
self._classFiltersTemp = {}
self._browseResults = {}
self._page = 0
end
function AuctionQuery.Release(self)
self._str = ""
self._strLower = ""
self._strMatch = ""
self._exact = false
self._minQuality = -math.huge
self._maxQuality = math.huge
self._minLevel = -math.huge
self._maxLevel = math.huge
self._minItemLevel = -math.huge
self._maxItemLevel = math.huge
self._class = nil
self._subClass = nil
self._invType = nil
wipe(self._classFilter1)
wipe(self._classFilter2)
self._usable = false
self._uncollected = false
self._upgrades = false
self._unlearned = false
self._canLearn = false
self._minPrice = 0
self._maxPrice = math.huge
wipe(self._items)
wipe(self._customFilters)
self._isBrowseDoneFunc = nil
self._specifiedPage = nil
self._getAll = nil
self._resolveSellers = false
self._callback = nil
wipe(self._queryTemp)
wipe(self._filtersTemp)
wipe(self._classFiltersTemp)
for _, row in pairs(self._browseResults) do
row:Release()
end
wipe(self._browseResults)
self._page = 0
private.objectPool:Recycle(self)
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AuctionQuery.SetStr(self, str, exact)
self._str = str or ""
self._strLower = strlower(self._str)
self._strMatch = String.Escape(self._strLower)
self._exact = exact or false
return self
end
function AuctionQuery.SetQualityRange(self, minQuality, maxQuality)
self._minQuality = minQuality or -math.huge
self._maxQuality = maxQuality or math.huge
return self
end
function AuctionQuery.SetLevelRange(self, minLevel, maxLevel)
self._minLevel = minLevel or -math.huge
self._maxLevel = maxLevel or math.huge
return self
end
function AuctionQuery.SetItemLevelRange(self, minItemLevel, maxItemLevel)
self._minItemLevel = minItemLevel or -math.huge
self._maxItemLevel = maxItemLevel or math.huge
return self
end
function AuctionQuery.SetClass(self, class, subClass, invType)
self._class = class or nil
self._subClass = subClass or nil
self._invType = invType or nil
return self
end
function AuctionQuery.SetUsable(self, usable)
self._usable = usable or false
return self
end
function AuctionQuery.SetUncollected(self, uncollected)
self._uncollected = uncollected or false
return self
end
function AuctionQuery.SetUpgrades(self, upgrades)
self._upgrades = upgrades or false
return self
end
function AuctionQuery.SetUnlearned(self, unlearned)
self._unlearned = unlearned or false
return self
end
function AuctionQuery.SetCanLearn(self, canLearn)
self._canLearn = canLearn or false
return self
end
function AuctionQuery.SetPriceRange(self, minPrice, maxPrice)
self._minPrice = minPrice or 0
self._maxPrice = maxPrice or math.huge
return self
end
function AuctionQuery.SetItems(self, items)
wipe(self._items)
if type(items) == "table" then
for _, itemString in ipairs(items) do
local baseItemString = ItemString.GetBaseFast(itemString)
self._items[itemString] = ITEM_SPECIFIC
if baseItemString ~= itemString then
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
end
end
elseif type(items) == "string" then
local itemString = items
local baseItemString = ItemString.GetBaseFast(itemString)
self._items[itemString] = ITEM_SPECIFIC
if baseItemString ~= itemString then
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
end
elseif items ~= nil then
error("Invalid items type: "..tostring(items))
end
return self
end
function AuctionQuery.AddCustomFilter(self, func)
self._customFilters[func] = true
return self
end
function AuctionQuery.SetIsBrowseDoneFunction(self, func)
self._isBrowseDoneFunc = func
return self
end
function AuctionQuery.SetPage(self, page)
if page == nil then
self._specifiedPage = nil
elseif type(page) == "number" or page == "FIRST" or page == "LAST" then
assert(TSM.IsWowClassic())
self._specifiedPage = page
else
error("Invalid page: "..tostring(page))
end
return self
end
function AuctionQuery.SetGetAll(self, getAll)
-- only currently support GetAll on classic
assert(not getAll or TSM.IsWowClassic())
self._getAll = getAll
return self
end
function AuctionQuery.SetResolveSellers(self, resolveSellers)
self._resolveSellers = resolveSellers
return self
end
function AuctionQuery.SetCallback(self, callback)
self._callback = callback
return self
end
function AuctionQuery.Browse(self, forceNoScan)
assert(not TSM.IsWowClassic() or not forceNoScan)
local noScan = forceNoScan or false
if not TSM.IsWowClassic() then
local numItems = 0
for _, itemType in pairs(self._items) do
if itemType == ITEM_SPECIFIC then
numItems = numItems + 1
end
end
if numItems > 0 and numItems < 500 then
-- it's faster to just issue individual item searches instead of a browse query
noScan = true
end
end
if noScan then
assert(not TSM.IsWowClassic())
local itemKeys = TempTable.Acquire()
for itemString in pairs(self._items) do
if itemString == ItemString.GetBaseFast(itemString) then
local itemId, battlePetSpeciesId = nil, nil
if ItemString.IsPet(itemString) then
itemId = ItemString.ToId(ItemString.GetPetCage())
battlePetSpeciesId = ItemString.ToId(itemString)
else
itemId = ItemString.ToId(itemString)
battlePetSpeciesId = 0
end
local itemKey = C_AuctionHouse.MakeItemKey(itemId, 0, 0, battlePetSpeciesId)
-- FIX for 9.0.1 bug where MakeItemKey randomly adds an itemLevel which breaks scanning
itemKey.itemLevel = 0
tinsert(itemKeys, itemKey)
end
end
local future = Scanner.BrowseNoScan(self, itemKeys, self._browseResults, self._callback)
TempTable.Release(itemKeys)
return future
else
self._page = 0
return Scanner.Browse(self, self._resolveSellers, self._browseResults, self._callback)
end
end
function AuctionQuery.GetSearchProgress(self)
if TSM.IsWowClassic() then
return 1
end
local progress, totalNum = 0, 0
for _, row in pairs(self._browseResults) do
progress = progress + row:_GetSearchProgress()
totalNum = totalNum + 1
end
if totalNum == 0 then
return 0
end
return progress / totalNum
end
function AuctionQuery.GetBrowseResults(self, baseItemString)
return self._browseResults[baseItemString]
end
function AuctionQuery.ItemSubRowIterator(self, itemString)
local result = TempTable.Acquire()
local baseItemString = ItemString.GetBaseFast(itemString)
local isBaseItemString = itemString == baseItemString
local row = self._browseResults[baseItemString]
if row then
for _, subRow in row:SubRowIterator() do
local subRowBaseItemString = subRow:GetBaseItemString()
local subRowItemString = subRow:GetItemString()
if (isBaseItemString and subRowBaseItemString == itemString) or (not isBaseItemString and subRowItemString == itemString) then
tinsert(result, subRow)
end
end
end
return TempTable.Iterator(result)
end
function AuctionQuery.GetCheapestSubRow(self, itemString)
assert(not TSM.IsWowClassic())
local cheapest, cheapestItemBuyout = nil, nil
for _, subRow in self:ItemSubRowIterator(itemString) do
local quantity = subRow:GetQuantities()
local _, numOwnerItems = subRow:GetOwnerInfo()
local _, itemBuyout = subRow:GetBuyouts()
if numOwnerItems ~= quantity and itemBuyout < (cheapestItemBuyout or math.huge) then
cheapest = subRow
cheapestItemBuyout = itemBuyout
end
end
return cheapest
end
function AuctionQuery.BrowseResultsIterator(self)
return pairs(self._browseResults)
end
function AuctionQuery.RemoveResultRow(self, row)
local baseItemString = row:GetBaseItemString()
assert(baseItemString and self._browseResults[baseItemString])
self._browseResults[baseItemString] = nil
row:Release()
if self._callback then
self._callback(self)
end
end
function AuctionQuery.Search(self, row, useCachedData)
assert(not TSM.IsWowClassic())
assert(self._browseResults)
return Scanner.Search(self, self._resolveSellers, useCachedData, row, self._callback)
end
function AuctionQuery.CancelBrowseOrSearch(self)
Scanner.Cancel()
end
function AuctionQuery.ItemIterator(self)
return private.ItemIteratorHelper, self._items, nil
end
function AuctionQuery.WipeBrowseResults(self)
for _, row in pairs(self._browseResults) do
row:Release()
end
wipe(self._browseResults)
if self._callback then
self._callback(self)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function AuctionQuery._SetSort(self)
if not TSM.IsWowClassic() then
return true
end
local sorts = (type(self._specifiedPage) == "string" or self._getAll) and EMPTY_SORTS or DEFAULT_SORTS
if GetAuctionSort("list", #sorts + 1) == nil then
local properlySorted = true
for i, col in ipairs(sorts) do
local sortCol, sortReversed = GetAuctionSort("list", #sorts - i + 1)
-- we never care to reverse a sort so if it's reversed then it's not properly sorted
if sortCol ~= col or sortReversed then
properlySorted = false
break
end
end
if properlySorted then
return true
end
end
SortAuctionClearSort("list")
for _, col in ipairs(sorts) do
SortAuctionSetSort("list", col, false)
end
SortAuctionApplySort("list")
return false
end
function AuctionQuery._SendWowQuery(self)
-- build the class filters
wipe(self._classFiltersTemp)
wipe(self._classFilter1)
wipe(self._classFilter2)
if self._invType == INV_TYPES.CHEST or self._invType == INV_TYPES.ROBE then
-- default AH only sends in queries for robe chest type, we need to mimic this when using a chest filter
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
self._classFilter1.subClassID = self._subClass
self._classFilter1.inventoryType = INV_TYPES.CHEST
tinsert(self._classFiltersTemp, self._classFilter1)
self._classFilter2.classID = LE_ITEM_CLASS_ARMOR
self._classFilter2.subClassID = self._subClass
self._classFilter2.inventoryType = INV_TYPES.ROBE
tinsert(self._classFiltersTemp, self._classFilter2)
elseif self._invType == INV_TYPES.NECK or self._invType == INV_TYPES.FINGER or self._invType == INV_TYPES.TRINKET or self._invType == INV_TYPES.HOLDABLE or self._invType == INV_TYPES.BODY then
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
self._classFilter1.subClassID = LE_ITEM_ARMOR_GENERIC
self._classFilter1.inventoryType = self._invType
tinsert(self._classFiltersTemp, self._classFilter1)
elseif self._invType == INV_TYPES.CLOAK then
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
self._classFilter1.subClassID = LE_ITEM_ARMOR_CLOTH
self._classFilter1.inventoryType = self._invType
tinsert(self._classFiltersTemp, self._classFilter1)
elseif self._class then
self._classFilter1.classID = self._class
self._classFilter1.subClassID = self._subClass
self._classFilter1.inventoryType = self._invType
tinsert(self._classFiltersTemp, self._classFilter1)
end
-- build the query
local minLevel = self._minLevel ~= -math.huge and self._minLevel or nil
local maxLevel = self._maxLevel ~= math.huge and self._maxLevel or nil
if TSM.IsWowClassic() then
if self._specifiedPage == "LAST" then
self._page = max(ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE) - 1, 0)
elseif self._specifiedPage == "FIRST" then
self._page = 0
elseif self._specifiedPage then
self._page = self._specifiedPage
end
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
return AuctionHouseWrapper.QueryAuctionItems(self._str, minLevel, maxLevel, self._page, self._usable, minQuality, self._getAll, self._exact, self._classFiltersTemp)
else
wipe(self._filtersTemp)
if self._uncollected then
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UncollectedOnly)
end
if self._usable then
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UsableOnly)
end
if self._upgrades then
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UpgradesOnly)
end
if self._exact then
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.ExactMatch)
end
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
for i = minQuality + Enum.AuctionHouseFilter.PoorQuality, min(self._maxQuality + Enum.AuctionHouseFilter.PoorQuality, Enum.AuctionHouseFilter.ArtifactQuality) do
tinsert(self._filtersTemp, i)
end
wipe(self._queryTemp)
self._queryTemp.searchString = self._str
self._queryTemp.minLevel = minLevel
self._queryTemp.maxLevel = maxLevel
self._queryTemp.sorts = DEFAULT_SORTS
self._queryTemp.filters = self._filtersTemp
self._queryTemp.itemClassFilters = self._classFiltersTemp
return AuctionHouseWrapper.SendBrowseQuery(self._queryTemp)
end
end
function AuctionQuery._IsFiltered(self, row, isSubRow, itemKey)
local baseItemString = row:GetBaseItemString()
local itemString = row:GetItemString()
assert(baseItemString)
local name, quality, itemLevel, maxItemLevel = row:GetItemInfo(itemKey)
local _, itemBuyout, minItemBuyout = row:GetBuyouts(itemKey)
if row:IsSubRow() and itemBuyout == 0 then
_, itemBuyout = row:GetBidInfo()
end
if next(self._items) then
if not self._items[baseItemString] then
return true
end
if isSubRow and itemString and self._items[itemString] ~= ITEM_SPECIFIC and self._items[baseItemString] ~= ITEM_SPECIFIC then
return true
elseif not isSubRow and itemString and not self._items[itemString] then
return true
end
end
if self._str ~= "" and name then
name = strlower(name)
if not strmatch(name, self._strMatch) or (self._exact and name ~= self._strLower) then
return true
end
end
if self._minLevel ~= -math.huge or self._maxLevel ~= math.huge then
local minLevel = TSM.IsShadowlands() and ItemString.IsPet(baseItemString) and (itemLevel or maxItemLevel) or ItemInfo.GetMinLevel(baseItemString)
if minLevel < self._minLevel or minLevel > self._maxLevel then
return true
end
end
if itemLevel and (itemLevel < self._minItemLevel or itemLevel > self._maxItemLevel) then
return true
end
if maxItemLevel and maxItemLevel < self._minItemLevel then
return true
end
if quality and (quality < self._minQuality or quality > self._maxQuality) then
return true
end
if self._class and ItemInfo.GetClassId(baseItemString) ~= self._class then
return true
end
if self._subClass and ItemInfo.GetSubClassId(baseItemString) ~= self._subClass then
return true
end
if self._invType and ItemInfo.GetInvSlotId(baseItemString) ~= self._invType then
return true
end
if self._unlearned and CanIMogIt:PlayerKnowsTransmog(ItemInfo.GetLink(baseItemString)) then
return true
end
if self._canLearn and not CanIMogIt:CharacterCanLearnTransmog(ItemInfo.GetLink(baseItemString)) then
return true
end
if itemBuyout and (itemBuyout < self._minPrice or itemBuyout > self._maxPrice) then
return true
end
if minItemBuyout and minItemBuyout > self._maxPrice then
return true
end
for func in pairs(self._customFilters) do
if func(self, row, isSubRow, itemKey) then
return true
end
end
return false
end
function AuctionQuery._BrowseIsDone(self, isRetry)
if TSM.IsWowClassic() then
local numAuctions, totalAuctions = GetNumAuctionItems("list")
if totalAuctions <= NUM_AUCTION_ITEMS_PER_PAGE and numAuctions ~= totalAuctions then
-- there are cases where we get (0, 1) from the API - no idea why so just assume we're not done
return false
end
local numPages = ceil(totalAuctions / NUM_AUCTION_ITEMS_PER_PAGE)
if self._getAll then
return true
end
if self._specifiedPage then
if isRetry then
return false
end
-- check if we're on the right page
local specifiedPage = (self._specifiedPage == "FIRST" and 0) or (self._specifiedPage == "LAST" and numPages - 1) or self._specifiedPage
return self._page == specifiedPage
elseif self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
return true
else
return self._page >= numPages
end
else
if self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
return true
end
return C_AuctionHouse.HasFullBrowseResults()
end
end
function AuctionQuery._BrowseIsPageValid(self)
if TSM.IsWowClassic() then
if self._specifiedPage then
return self:_BrowseIsDone()
else
return true
end
else
return true
end
end
function AuctionQuery._BrowseRequestMore(self, isRetry)
if TSM.IsWowClassic() then
assert(not self._getAll)
if self._specifiedPage then
return self:_SendWowQuery()
end
if not isRetry then
self._page = self._page + 1
end
return self:_SendWowQuery()
else
return AuctionHouseWrapper.RequestMoreBrowseResults()
end
end
function AuctionQuery._OnSubRowRemoved(self, row)
local baseItemString = row:GetBaseItemString()
assert(row == self._browseResults[baseItemString])
if row:GetNumSubRows() == 0 then
self._browseResults[baseItemString] = nil
row:Release()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ItemIteratorHelper(items, index)
while true do
local itemString, itemType = next(items, index)
if not itemString then
return
elseif itemType == ITEM_SPECIFIC then
return itemString
end
index = itemString
end
end