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