initial commit

This commit is contained in:
Gitea
2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

View File

@@ -0,0 +1,675 @@
-- ------------------------------------------------------------------------------ --
-- 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

View File

@@ -0,0 +1,134 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local QueryUtil = TSM.Init("Service.AuctionScanClasses.QueryUtil")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Query = TSM.Include("Service.AuctionScanClasses.Query")
local private = {
itemListSortValue = {},
}
local MAX_ITEM_INFO_RETRIES = 30
-- ============================================================================
-- Module Functions
-- ============================================================================
function QueryUtil.GenerateThreaded(itemList, callback, context)
-- get all the item info into the game's cache
for _ = 1, MAX_ITEM_INFO_RETRIES do
local isMissingItemInfo = false
for _, itemString in ipairs(itemList) do
if not private.HasInfo(itemString) then
isMissingItemInfo = true
end
Threading.Yield()
end
if not isMissingItemInfo then
break
end
Threading.Sleep(0.1)
end
-- remove items we're missing info for
for i = #itemList, 1, -1 do
if not private.HasInfo(itemList[i]) then
Log.Err("Missing item info for %s", itemList[i])
tremove(itemList, i)
end
Threading.Yield()
end
if #itemList == 0 then
return
end
-- add all the items
if TSM.IsWowClassic() then
for _, itemString in ipairs(itemList) do
private.GenerateQuery(callback, context, itemString, private.GetItemQueryInfo(itemString))
end
else
-- sort the item list so all base items are grouped together but keep relative ordering between base items the same
wipe(private.itemListSortValue)
for i, itemString in ipairs(itemList) do
local baseItemString = ItemString.GetBaseFast(itemString)
private.itemListSortValue[baseItemString] = private.itemListSortValue[baseItemString] or i
private.itemListSortValue[itemString] = private.itemListSortValue[baseItemString]
end
sort(itemList, private.ItemListSortHelper)
local currentBaseItemString = nil
local currentItems = TempTable.Acquire()
for _, itemString in ipairs(itemList) do
local baseItemString = ItemString.GetBaseFast(itemString)
assert(baseItemString)
if baseItemString == currentBaseItemString then
-- same base item
tinsert(currentItems, itemString)
else
-- new base item
if currentBaseItemString then
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
wipe(currentItems)
end
currentBaseItemString = baseItemString
tinsert(currentItems, itemString)
end
end
if currentBaseItemString then
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
wipe(currentItems)
end
TempTable.Release(currentItems)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetItemQueryInfo(itemString)
local name = ItemInfo.GetName(itemString)
local level = ItemInfo.GetMinLevel(itemString) or 0
local quality = ItemInfo.GetQuality(itemString)
local classId = ItemInfo.GetClassId(itemString) or 0
local subClassId = ItemInfo.GetSubClassId(itemString) or 0
-- Ignoring level because level can now vary
if itemString == ItemString.GetBase(itemString) and (classId == LE_ITEM_CLASS_WEAPON or classId == LE_ITEM_CLASS_ARMOR or (classId == LE_ITEM_CLASS_GEM and subClassId == LE_ITEM_GEM_ARTIFACTRELIC)) then
level = nil
end
return name, level, level, quality, classId, subClassId
end
function private.HasInfo(itemString)
return ItemInfo.GetName(itemString) and ItemInfo.GetQuality(itemString) and ItemInfo.GetMinLevel(itemString)
end
function private.GenerateQuery(callback, context, items, name, minLevel, maxLevel, quality, class, subClass)
local query = Query.Get()
:SetStr(name, false)
:SetQualityRange(quality, quality)
:SetLevelRange(minLevel, maxLevel)
:SetClass(class, subClass)
:SetItems(items)
callback(query, context)
end
function private.ItemListSortHelper(a, b)
local aSortValue = private.itemListSortValue[a]
local bSortValue = private.itemListSortValue[b]
if aSortValue ~= bSortValue then
return aSortValue < bSortValue
end
return a < b
end

View File

@@ -0,0 +1,671 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ResultRow = TSM.Init("Service.AuctionScanClasses.ResultRow")
local ItemString = TSM.Include("Util.ItemString")
local ObjectPool = TSM.Include("Util.ObjectPool")
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local ItemInfo = TSM.Include("Service.ItemInfo")
local LibTSMClass = TSM.Include("LibTSMClass")
local Util = TSM.Include("Service.AuctionScanClasses.Util")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local ResultSubRow = TSM.Include("Service.AuctionScanClasses.ResultSubRow")
local ResultRowWrapper = LibTSMClass.DefineClass("ResultRowWrapper")
local private = {
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_ROW", ResultRowWrapper),
}
local SUB_ROW_SEARCH_INDEX_MULTIPLIER = 1000000
-- ============================================================================
-- Module Functions
-- ============================================================================
function ResultRow.Get(query, itemKey, minPrice, totalQuantity)
local row = private.objectPool:Get()
row:_Acquire(query, itemKey, minPrice, totalQuantity)
return row
end
-- ============================================================================
-- ResultRowWrapper - Meta Class Methods
-- ============================================================================
function ResultRowWrapper.__init(self)
self._query = nil
self._items = {}
self._baseItemString = nil
self._canHaveNonBaseItemString = nil
self._minPrice = nil
self._hasItemInfo = nil
self._isCommodity = nil
self._notFiltered = false
self._searchIndex = nil
self._subRows = {}
self._minBrowseId = nil
end
function ResultRowWrapper._Acquire(self, query, item, minPrice, totalQuantity)
self._query = query
if TSM.IsWowClassic() then
assert(not minPrice and not totalQuantity)
tinsert(self._items, item)
self._baseItemString = ItemString.GetBase(item)
else
item._minPrice = minPrice
item._totalQuantity = totalQuantity
tinsert(self._items, item)
self._baseItemString = ItemString.GetBaseFromItemKey(item)
end
self._canHaveNonBaseItemString = nil
self._minPrice = nil
end
-- ============================================================================
-- ResultRowWrapper - Public Class Methods
-- ============================================================================
function ResultRowWrapper.Merge(self, item, minPrice, totalQuantity)
-- check if we already have this item
for i = 1, #self._items do
if item == self._items[i] then
return
end
if type(item) == "table" then
local isEqual = true
for k in pairs(item) do
if item[k] ~= self._items[i][k] then
isEqual = false
break
end
end
if isEqual then
return
end
end
end
self._hasItemInfo = nil
if TSM.IsWowClassic() then
assert(not minPrice and not totalQuantity)
assert(self._baseItemString == ItemString.GetBase(item))
tinsert(self._items, item)
self._notFiltered = false
else
assert(self._baseItemString == ItemString.GetBaseFromItemKey(item))
item._minPrice = minPrice
item._totalQuantity = totalQuantity
tinsert(self._items, item)
self._notFiltered = false
end
self._canHaveNonBaseItemString = nil
end
function ResultRowWrapper.Release(self)
wipe(self._items)
self._baseItemString = nil
self._canHaveNonBaseItemString = nil
self._minPrice = nil
self._hasItemInfo = nil
self._isCommodity = nil
self._notFiltered = false
self._searchIndex = nil
self._minBrowseId = nil
for _, subRow in pairs(self._subRows) do
subRow:Release()
end
wipe(self._subRows)
private.objectPool:Recycle(self)
end
function ResultRowWrapper.IsSubRow(self)
return false
end
function ResultRowWrapper.PopulateBrowseData(self)
assert(self._baseItemString)
if self._hasItemInfo then
-- already have our item info
return true
elseif not Util.HasItemInfo(self._baseItemString) then
-- don't have item info yet
return false
end
if not TSM.IsWowClassic() then
-- cache the commodity status since it's referenced a ton
if self._isCommodity == nil then
self._isCommodity = ItemInfo.IsCommodity(self._baseItemString)
assert(self._isCommodity ~= nil)
end
end
-- check if we have info for all the items and try to fetch it if not
local missingInfo = false
for _, item in ipairs(self._items) do
if TSM.IsWowClassic() then
if not Util.HasItemInfo(ItemString.Get(item)) then
missingInfo = true
end
else
if not item._itemKeyInfo then
item._itemKeyInfo = C_AuctionHouse.GetItemKeyInfo(item, true)
if not item._itemKeyInfo then
missingInfo = true
end
end
end
end
if missingInfo then
return false
end
self._hasItemInfo = true
return true
end
function ResultRowWrapper.IsFiltered(self, query)
assert(#self._items > 0)
if self._notFiltered then
return false
end
-- check if the whole row is filtered
if query:_IsFiltered(self, false) then
return true
end
-- filter our items
for i = #self._items, 1, -1 do
if query:_IsFiltered(self, false, self._items[i]) then
tremove(self._items, i)
end
end
self._canHaveNonBaseItemString = nil
self._minPrice = nil
if #self._items == 0 then
-- no more items, so the entire row is filtered
return true
end
-- not filtered (cache this result)
self._notFiltered = true
return false
end
function ResultRowWrapper.SearchReset(self)
assert(not TSM.IsWowClassic())
assert(#self._items > 0)
self._searchIndex = 1
end
function ResultRowWrapper.SearchNext(self)
assert(not TSM.IsWowClassic())
assert(self._searchIndex)
if self._searchIndex == #self._items then
self._searchIndex = nil
return false
end
self._searchIndex = self._searchIndex + 1
return true
end
function ResultRowWrapper.SearchIsReady(self)
assert(not TSM.IsWowClassic())
assert(self._searchIndex)
-- the client needs to have the item key info cached before we can run the search
return C_AuctionHouse.GetItemKeyInfo(self._items[self._searchIndex], true) and true or false
end
function ResultRowWrapper.SearchSend(self)
assert(not TSM.IsWowClassic())
assert(self._searchIndex)
local itemKey = self._items[self._searchIndex]
-- send a sell query if we don't have browse results for the itemKey
-- for some reason sell queries don't work for commodities or pets
local isSellQuery = not self._isCommodity and not ItemString.IsPet(self._baseItemString) and not itemKey._totalQuantity
return AuctionHouseWrapper.SendSearchQuery(itemKey, isSellQuery)
end
function ResultRowWrapper.HasCachedSearchData(self)
local itemKey = self._items[self._searchIndex]
if self._isCommodity then
return C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
else
return C_AuctionHouse.HasFullItemSearchResults(itemKey)
end
end
function ResultRowWrapper.SearchCheckStatus(self)
assert(not TSM.IsWowClassic())
assert(self._searchIndex)
local itemKey = self._items[self._searchIndex]
-- check if we have the full results
local hasFullResults = nil
if self._isCommodity then
hasFullResults = C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
else
hasFullResults = C_AuctionHouse.HasFullItemSearchResults(itemKey)
end
if hasFullResults then
return true
end
-- request more results
if self._isCommodity then
return false, AuctionHouseWrapper.RequestMoreCommoditySearchResults(itemKey.itemID)
else
return false, AuctionHouseWrapper.RequestMoreItemSearchResults(itemKey)
end
end
function ResultRowWrapper.PopulateSubRows(self, browseId, index, itemLink)
if TSM.IsWowClassic() then
-- remove any prior results with a different browseId
assert(index and not self._searchIndex)
local subRow = ResultSubRow.Get(self)
subRow:_SetRawData(index, browseId, itemLink)
local _, hashNoSeller = subRow:GetHashes()
if self._minBrowseId and self._minBrowseId ~= browseId then
-- check if this subRow already exists with a prior browseId
for i, existingSubRow in ipairs(self._subRows) do
local _, existingHashNoSeller = existingSubRow:GetHashes()
local _, _, existingBrowseId = existingSubRow:GetListingInfo()
if hashNoSeller == existingHashNoSeller and browseId ~= existingBrowseId then
-- replace the existing subRow
existingSubRow:Release()
self._subRows[i] = subRow
return
end
end
end
tinsert(self._subRows, subRow)
else
assert(self._searchIndex and not index)
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
local itemKey = self._items[self._searchIndex]
local numAuctions = nil
if self:IsCommodity() then
numAuctions = C_AuctionHouse.GetNumCommoditySearchResults(itemKey.itemID)
else
numAuctions = C_AuctionHouse.GetNumItemSearchResults(itemKey)
end
if itemKey._numAuctions and numAuctions ~= itemKey._numAuctions then
-- the results changed so clear out our existing data
for i = itemKey._numAuctions, 1, -1 do
if i > numAuctions then
self._subRows[subRowOffset + i]:Release()
self._subRows[subRowOffset + i] = nil
else
self._subRows[subRowOffset + i]:_SetRawData(nil)
end
end
end
itemKey._numAuctions = numAuctions
for i = 1, numAuctions do
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i] or ResultSubRow.Get(self)
local subRow = self._subRows[subRowOffset + i]
if not subRow:HasRawData() or not subRow:HasOwners() then
local result = nil
if self:IsCommodity() then
result = C_AuctionHouse.GetCommoditySearchResultInfo(itemKey.itemID, i)
else
result = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
end
subRow:_SetRawData(result, browseId)
end
end
end
self._minBrowseId = min(self._minBrowseId or math.huge, browseId)
end
function ResultRowWrapper.FilterSubRows(self, query)
local subRowOffset = TSM.IsWowClassic() and 0 or (self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER)
if TSM.IsWowClassic() then
for i = #self._subRows, 1, -1 do
if query:_IsFiltered(self._subRows[i], true) then
self:_RemoveSubRowByIndex(i)
end
end
else
local itemKey = self._items[self._searchIndex]
for j = itemKey._numAuctions, 1, -1 do
local subRow = self._subRows[subRowOffset + j]
if query:_IsFiltered(subRow, true) then
self:_RemoveSubRowByIndex(j)
end
end
end
-- merge subRows with identical hashes
local numSubRows = nil
local hashIndexLookup = TempTable.Acquire()
local index = 1
while true do
numSubRows = TSM.IsWowClassic() and #self._subRows or self._items[self._searchIndex]._numAuctions
if index > numSubRows then
break
end
local subRow = self._subRows[subRowOffset + index]
local hash = subRow:GetHashes()
local prevIndex = hashIndexLookup[hash]
if prevIndex then
-- there was a previous subRow with the same hash
self._subRows[subRowOffset + prevIndex]:Merge(subRow)
-- remove this subRow
self:_RemoveSubRowByIndex(index)
else
hashIndexLookup[hash] = index
index = index + 1
end
end
TempTable.Release(hashIndexLookup)
return numSubRows == 0
end
function ResultRowWrapper.GetNumSubRows(self)
if TSM.IsWowClassic() then
return #self._subRows
else
local result = 0
for _, itemKey in ipairs(self._items) do
result = result + (itemKey._numAuctions or 0)
end
return result
end
end
function ResultRowWrapper.SubRowIterator(self, searchOnly)
if TSM.IsWowClassic() then
return ipairs(self._subRows)
else
if searchOnly then
local result = TempTable.Acquire()
assert(self._searchIndex)
for i = 1, self._items[self._searchIndex]._numAuctions do
local subRow = self._subRows[self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + i]
assert(subRow)
tinsert(result, subRow)
end
return TempTable.Iterator(result)
else
return private.SubRowIteratorHelper, self, SUB_ROW_SEARCH_INDEX_MULTIPLIER
end
end
end
function ResultRowWrapper.IsCommodity(self)
assert(self._isCommodity ~= nil)
return self._isCommodity
end
function ResultRowWrapper.HasItemInfo(self)
return self._hasItemInfo
end
function ResultRowWrapper.GetBaseItemString(self)
return self._baseItemString
end
function ResultRowWrapper.GetItemString(self)
if TSM.IsWowClassic() or not self._hasItemInfo or self._canHaveNonBaseItemString then
return nil
end
if self._canHaveNonBaseItemString == nil then
for _, itemKey in ipairs(self._items) do
if ItemInfo.CanHaveVariations(self._baseItemString) or itemKey.battlePetSpeciesID ~= 0 or itemKey.itemSuffix ~= 0 or itemKey.itemLevel ~= 0 then
-- this item can have variations, so we don't know its itemString
self._canHaveNonBaseItemString = true
return nil
end
end
self._canHaveNonBaseItemString = false
end
return self._baseItemString
end
function ResultRowWrapper.GetItemInfo(self, itemKey)
if TSM.IsWowClassic() or not self._hasItemInfo then
return nil, nil, nil, nil
end
itemKey = itemKey or (#self._items == 1 and self._items[1] or nil)
assert(not itemKey or itemKey._itemKeyInfo)
local baseItemString = self:GetBaseItemString()
local itemString = self:GetItemString()
local itemName, quality, itemLevel, maxItemLevel = nil, nil, nil, nil
if itemString then
-- this item can't have variations, so we can know the name / level / quality
itemName = ItemInfo.GetName(baseItemString)
itemLevel = ItemInfo.GetItemLevel(baseItemString)
quality = ItemInfo.GetQuality(baseItemString)
assert(itemName and itemLevel and quality)
else
if itemKey and not itemKey._totalQuantity then
-- if we didn't do a browse, then don't use this itemKey
itemKey = nil
end
if itemKey then
-- grab the name from the itemKeyInfo
itemName = itemKey._itemKeyInfo.itemName
assert(itemName)
end
local hasSingleAuction = itemKey and itemKey._totalQuantity == 1
if hasSingleAuction then
-- grab the quality from the itemKeyInfo since there's only one listing
quality = itemKey._itemKeyInfo.quality
assert(quality)
end
if not ItemString.IsPet(self._baseItemString) then
-- for non-pets, we can maybe grab the itemLevel from the itemKey
if itemKey then
itemLevel = itemKey.itemLevel ~= 0 and itemKey.itemLevel or nil
else
-- only use the itemLevel from the itemKeys if they are all the same
local itemKeyItemLevel = self._items[1].itemLevel
for i = 2, #self._items do
if self._items[i].itemLevel ~= itemKeyItemLevel then
itemKeyItemLevel = nil
break
end
end
itemLevel = (itemKeyItemLevel or 0) ~= 0 and itemKeyItemLevel or nil
end
elseif itemKey and itemKey._itemKeyInfo.battlePetLink then
if hasSingleAuction then
-- grab the itemLevel from the link since there's only one listing
itemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
assert(itemLevel)
else
-- grab the maxItemLevel from the link
maxItemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
assert(maxItemLevel)
end
end
end
return itemName, quality, itemLevel, maxItemLevel
end
function ResultRowWrapper.GetBuyouts(self, resultItemKey)
if TSM.IsWowClassic() then
return nil, nil, nil
end
assert(#self._items > 0)
if resultItemKey then
return nil, nil, resultItemKey._minPrice
else
if self._minPrice == nil then
for _, itemKey in ipairs(self._items) do
if not itemKey._minPrice then
self._minPrice = -1
return nil, nil, nil
end
self._minPrice = min(self._minPrice or math.huge, itemKey._minPrice)
end
elseif self._minPrice == -1 then
return nil, nil, nil
end
return nil, nil, self._minPrice
end
end
function ResultRowWrapper.GetQuantities(self)
local totalQuantity = 0
if TSM.IsWowClassic() then
for _, subRow in ipairs(self._subRows) do
local quantity, numAuctions = subRow:GetQuantities()
totalQuantity = totalQuantity + quantity * numAuctions
end
else
for _, itemKey in ipairs(self._items) do
if not itemKey._totalQuantity then
return
end
totalQuantity = totalQuantity + itemKey._totalQuantity
end
end
return totalQuantity, 1
end
function ResultRowWrapper.GetMaxQuantities(self)
assert(self:IsCommodity())
local totalQuantity = 0
for _, subRow in self:SubRowIterator() do
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
totalQuantity = totalQuantity + quantityAvailable
end
return totalQuantity
end
function ResultRowWrapper.RemoveSubRow(self, subRow)
local index = Table.KeyByValue(self._subRows, subRow)
if TSM.IsWowClassic() then
self:_RemoveSubRowByIndex(index)
else
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
index = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
assert(self._subRows[searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + index] == subRow)
local prevSearchIndex = self._searchIndex
self._searchIndex = searchIndex
self:_RemoveSubRowByIndex(index)
self._searchIndex = prevSearchIndex
end
self._query:_OnSubRowRemoved(self)
end
function ResultRowWrapper.WipeSearchResults(self)
wipe(self._subRows)
if not TSM.IsWowClassic() then
for _, itemKey in ipairs(self._items) do
itemKey._numAuctions = nil
end
end
end
function ResultRowWrapper.GetQuery(self)
return self._query
end
function ResultRowWrapper.DecrementQuantity(self, amount)
assert(self:IsCommodity() and not TSM.IsWowClassic() and #self._items == 1)
local index = 1
while amount > 0 do
local subRow = self._subRows[index + SUB_ROW_SEARCH_INDEX_MULTIPLIER]
assert(subRow)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
if quantityAvailable > 0 then
local usedQuantity = min(quantityAvailable, amount)
local prevItemBuyout = floor(subRow._buyout / subRow._quantity)
amount = amount - usedQuantity
subRow._quantity = subRow._quantity - usedQuantity
subRow._buyout = prevItemBuyout * subRow._quantity
subRow._minBid = subRow._buyout
if numOwnerItems == 0 and subRow._quantity == 0 then
self:RemoveSubRow(subRow)
else
index = index + 1
end
else
index = index + 1
end
end
end
function ResultRowWrapper.GetMinBrowseId(self)
return self._minBrowseId
end
-- ============================================================================
-- ResultRowWrapper - Private Class Methods
-- ============================================================================
function ResultRowWrapper._RemoveSubRowByIndex(self, index)
if TSM.IsWowClassic() then
self._subRows[index]:Release()
tremove(self._subRows, index)
else
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
local itemKey = self._items[self._searchIndex]
self._subRows[subRowOffset + index]:Release()
self._subRows[subRowOffset + index] = nil
-- shift the other subRows for this item down
for i = index, itemKey._numAuctions - 1 do
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i + 1]
end
self._subRows[subRowOffset + itemKey._numAuctions] = nil
itemKey._numAuctions = itemKey._numAuctions - 1
end
end
function ResultRowWrapper._GetSearchProgress(self)
assert(not TSM.IsWowClassic())
if #self._items == 0 then
return 0
end
local numSearched = 0
for _, itemKey in ipairs(self._items) do
if itemKey._numAuctions then
numSearched = numSearched + 1
end
end
return numSearched / #self._items
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubRowIteratorHelper(row, index)
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
local subRowIndex = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
while true do
local itemKey = row._items[searchIndex]
if not itemKey then
return
end
if subRowIndex >= (itemKey._numAuctions or 0) then
searchIndex = searchIndex + 1
subRowIndex = 0
else
subRowIndex = subRowIndex + 1
index = searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + subRowIndex
return index, row._subRows[index]
end
end
end

View File

@@ -0,0 +1,364 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ResultSubRow = TSM.Init("Service.AuctionScanClasses.ResultSubRow")
local ItemString = TSM.Include("Util.ItemString")
local ObjectPool = TSM.Include("Util.ObjectPool")
local Math = TSM.Include("Util.Math")
local ItemInfo = TSM.Include("Service.ItemInfo")
local LibTSMClass = TSM.Include("LibTSMClass")
local Util = TSM.Include("Service.AuctionScanClasses.Util")
local ResultSubRowWrapper = LibTSMClass.DefineClass("ResultSubRowWrapper")
local private = {
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_SUB_ROW", ResultSubRowWrapper),
ownersTemp = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function ResultSubRow.Get(resultRow)
local subRow = private.objectPool:Get()
subRow:_Acquire(resultRow)
return subRow
end
-- ============================================================================
-- ResultSubRowWrapper - Meta Class Methods
-- ============================================================================
function ResultSubRowWrapper.__init(self)
self._resultRow = nil
self._itemLink = nil
self._buyout = nil
self._minBid = nil
self._currentBid = nil
self._minIncrement = nil
self._isHighBidder = nil
self._quantity = nil
self._timeLeft = nil
self._ownerStr = nil
self._hasOwners = false
self._numOwnerItems = nil
self._auctionId = nil
self._hash = nil
self._hashNoSeller = nil
self._browseId = nil
self._numAuctions = 1
end
function ResultSubRowWrapper._Acquire(self, resultRow)
self._resultRow = resultRow
end
-- ============================================================================
-- ResultSubRowWrapper - Public Class Methods
-- ============================================================================
function ResultSubRowWrapper.Merge(self, other)
if TSM.IsWowClassic() then
self._numAuctions = self._numAuctions + other._numAuctions
else
if self:IsCommodity() then
self._quantity = self._quantity + other._quantity
self._numOwnerItems = self._numOwnerItems + other._numOwnerItems
else
self._numAuctions = self._numAuctions + other._numAuctions
end
end
end
function ResultSubRowWrapper.Release(self)
self._resultRow = nil
self._numAuctions = 1
self:_SetRawData(nil)
private.objectPool:Recycle(self)
end
function ResultSubRowWrapper.IsSubRow(self)
return true
end
function ResultSubRowWrapper.HasRawData(self)
return self._timeLeft and true or false
end
function ResultSubRowWrapper.HasOwners(self)
return self._hasOwners
end
function ResultSubRowWrapper.HasItemString(self)
assert(self:HasRawData())
local itemString = ItemString.Get(self._itemLink)
if not Util.HasItemInfo(itemString) then
return false
end
return true
end
function ResultSubRowWrapper.IsCommodity(self)
return self._resultRow:IsCommodity()
end
function ResultSubRowWrapper.GetResultRow(self)
return self._resultRow
end
function ResultSubRowWrapper.GetBaseItemString(self)
return self._resultRow:GetBaseItemString()
end
function ResultSubRowWrapper.GetItemString(self)
assert(self:HasRawData())
local itemString = ItemString.Get(self._itemLink)
return itemString or self._resultRow:GetItemString()
end
function ResultSubRowWrapper.GetItemInfo(self)
assert(self:HasItemString())
local itemString = ItemString.Get(self._itemLink)
local itemName = ItemInfo.GetName(itemString)
local quality = ItemInfo.GetQuality(itemString)
local itemLevel = ItemInfo.GetItemLevel(itemString)
assert(itemName and quality and itemLevel)
return itemName, quality, itemLevel, nil
end
function ResultSubRowWrapper.GetBuyouts(self)
assert(self:HasRawData())
return self._buyout, floor(self._buyout / self._quantity), nil
end
function ResultSubRowWrapper.GetBidInfo(self)
assert(self:HasRawData())
local itemMinBid = Math.Floor(self._minBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
return self._minBid, itemMinBid, self._currentBid, self._isHighBidder, self._minIncrement
end
function ResultSubRowWrapper.GetRequiredBid(self)
local requiredBid = nil
if TSM.IsWowClassic() then
requiredBid = self._currentBid == 0 and self._minBid or (self._currentBid + self._minIncrement)
else
requiredBid = self._minBid
end
return requiredBid
end
function ResultSubRowWrapper.GetDisplayedBids(self)
local displayedBid = self._currentBid == 0 and self._minBid or self._currentBid
local itemDisplayedBid = Math.Floor(displayedBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
return displayedBid, itemDisplayedBid
end
function ResultSubRowWrapper.GetLinks(self)
assert(self:HasRawData())
local rawLink = self._itemLink
local itemLink = ItemInfo.GeneralizeLink(rawLink)
return itemLink, rawLink
end
function ResultSubRowWrapper.GetListingInfo(self)
assert(self:HasRawData())
return self._timeLeft, self._auctionId, self._browseId
end
function ResultSubRowWrapper.GetQuantities(self)
assert(self:HasRawData())
return self._quantity, self._numAuctions
end
function ResultSubRowWrapper.GetOwnerInfo(self)
assert(self:HasRawData())
return self._ownerStr, self._numOwnerItems
end
function ResultSubRowWrapper.GetHashes(self)
if not self._hash then
assert(self:HasRawData())
if TSM.IsWowClassic() then
self._hash = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._ownerStr, self._timeLeft, self._quantity, self._isHighBidder))
self._hashNoSeller = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._timeLeft, self._quantity, self._isHighBidder))
else
local baseItemString = self:GetBaseItemString()
local itemMinBid = Math.Floor(self._minBid / self._quantity, COPPER_PER_SILVER)
local itemBuyout = floor(self._buyout / self._quantity)
local itemKeyId, itemKeySpeciesId = nil, nil
if ItemString.IsPet(baseItemString) then
itemKeyId = ItemString.ToId(ItemString.GetPetCage())
itemKeySpeciesId = ItemString.ToId(baseItemString)
elseif ItemString.IsItem(baseItemString) then
itemKeyId = ItemString.ToId(baseItemString)
itemKeySpeciesId = 0
else
error("Invalid baseItemString: "..tostring(baseItemString))
end
if self:IsCommodity() then
self._hash = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId, self._ownerStr))
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId))
else
self._hash = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._ownerStr, self._auctionId))
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._auctionId))
end
end
end
return self._hash, self._hashNoSeller
end
function ResultSubRowWrapper.EqualsIndex(self, index, noSeller)
assert(TSM.IsWowClassic())
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", index)
seller = Util.FixSellerName(seller, sellerFull) or "?"
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
minIncrement = isHighBidder and 0 or minIncrement
if minBid ~= self._minBid or minIncrement ~= self._minIncrement or buyout ~= self._buyout or bid ~= self._currentBid or stackSize == self._quantity and isHighBidder ~= self._isHighBidder then
return false
elseif not noSeller and seller ~= self._ownerStr then
return false
elseif GetAuctionItemLink("list", index) ~= self._itemLink then
return false
elseif GetAuctionItemTimeLeft("list", index) ~= self._timeLeft then
return false
end
return true
end
function ResultSubRowWrapper.DecrementQuantity(self, amount)
if TSM.IsWowClassic() then
assert(amount == self._quantity)
self._numAuctions = self._numAuctions - 1
if self._numAuctions == 0 then
self._resultRow:RemoveSubRow(self)
end
else
if self:IsCommodity() then
self._resultRow:DecrementQuantity(amount)
else
assert(amount == 1 and amount == self._quantity)
self._numAuctions = self._numAuctions - 1
assert(self._numOwnerItems <= self._numAuctions)
if self._numAuctions == 0 then
self._resultRow:RemoveSubRow(self)
end
end
end
end
function ResultSubRowWrapper.UpdateResultInfo(self, newAuctionId, newResultInfo)
if newResultInfo then
self:_SetRawData(newResultInfo, self._browseId)
else
self._auctionId = newAuctionId
self._hash = nil
self._hashNoSeller = nil
end
end
-- ============================================================================
-- ResultRowWrapper - Private Class Methods
-- ============================================================================
function ResultSubRowWrapper._SetRawData(self, data, browseId, itemLink)
self._hash = nil
self._hashNoSeller = nil
self._browseId = browseId
if data then
if TSM.IsWowClassic() then
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", data)
seller = Util.FixSellerName(seller, sellerFull)
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
minIncrement = isHighBidder and 0 or minIncrement
self._itemLink = itemLink
self._buyout = buyout
self._minBid = minBid
self._currentBid = bid
self._minIncrement = minIncrement
self._isHighBidder = isHighBidder
self._quantity = stackSize
self._timeLeft = GetAuctionItemTimeLeft("list", data)
self._ownerStr = seller or "?"
self._hasOwners = seller and true or false
self._numOwnerItems = 0
self._auctionId = 0
else
if self._resultRow:IsCommodity() then
local baseItemString = self._resultRow:GetBaseItemString()
self._itemLink = ItemInfo.GetLink(baseItemString)
else
self._itemLink = data.itemLink
end
if self:IsCommodity() then
self._quantity = data.quantity
self._buyout = data.unitPrice * data.quantity
self._minBid = self._buyout
self._currentBid = 0
self._minIncrement = 0
self._isHighBidder = data.bidder and data.bidder == UnitGUID("player") or false
self._numOwnerItems = data.numOwnerItems or 0
-- convert the timeLeftSeconds to regular timeLeft
if data.timeLeftSeconds < 60 * 60 then
self._timeLeft = 1
elseif data.timeLeftSeconds < 2 * 60 * 60 then
self._timeLeft = 2
elseif data.timeLeftSeconds < 12 * 60 * 60 then
self._timeLeft = 3
else
self._timeLeft = 4
end
else
self._quantity = 1
self._numAuctions = data.quantity
self._buyout = data.buyoutAmount or 0
self._minBid = data.minBid or data.buyoutAmount
self._currentBid = data.bidAmount or 0
self._minIncrement = 0
self._isHighBidder = false
self._numOwnerItems = data.containsAccountItem and data.quantity or 0
self._timeLeft = data.timeLeft + 1
end
self._hasOwners = #data.owners > 0
assert(#private.ownersTemp == 0)
for _, owner in ipairs(data.owners) do
if owner == "player" then
owner = UnitName("player")
elseif owner == "" then
owner = "?"
self._hasOwners = false
end
tinsert(private.ownersTemp, owner)
end
self._ownerStr = table.concat(private.ownersTemp, ",")
wipe(private.ownersTemp)
self._auctionId = data.auctionID
end
assert(self._itemLink and self._quantity and self._buyout and self._minBid and self._currentBid and self._numOwnerItems and self._timeLeft and self._ownerStr and self._auctionId)
else
self._itemLink = nil
self._buyout = nil
self._minBid = nil
self._currentBid = nil
self._minIncrement = nil
self._isHighBidder = nil
self._quantity = nil
self._timeLeft = nil
self._ownerStr = nil
self._hasOwners = false
self._numOwnerItems = nil
self._auctionId = nil
end
end

View File

@@ -0,0 +1,620 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- This file contains code for scanning the auction house
local _, TSM = ...
local ScanManager = TSM.Init("Service.AuctionScanClasses.ScanManager")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Math = TSM.Include("Util.Math")
local ObjectPool = TSM.Include("Util.ObjectPool")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Query = TSM.Include("Service.AuctionScanClasses.Query")
local QueryUtil = TSM.Include("Service.AuctionScanClasses.QueryUtil")
local AuctionScanManager = TSM.Include("LibTSMClass").DefineClass("AuctionScanManager")
local private = {
objectPool = ObjectPool.New("AUCTION_SCAN_MANAGER", AuctionScanManager),
}
-- arbitrary estimate that finishing the browse request is worth 10% of the query's progress
local BROWSE_PROGRESS = 0.1
-- ============================================================================
-- Module Functions
-- ============================================================================
function ScanManager.Get()
return private.objectPool:Get()
end
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function AuctionScanManager.__init(self)
self._resolveSellers = nil
self._ignoreItemLevel = nil
self._queries = {}
self._queriesScanned = 0
self._queryDidBrowse = false
self._onProgressUpdateHandler = nil
self._onQueryDoneHandler = nil
self._resultsUpdateCallbacks = {}
self._nextSearchItemFunction = nil
self._currentSearchChangedCallback = nil
self._findResult = {}
self._cancelled = false
self._shouldPause = false
self._paused = false
self._scanQuery = nil
self._findQuery = nil
self._numItems = nil
self._queryCallback = function(query, searchRow)
for func in pairs(self._resultsUpdateCallbacks) do
func(self, query, searchRow)
end
end
end
function AuctionScanManager._Release(self)
self._resolveSellers = nil
self._ignoreItemLevel = nil
for _, query in ipairs(self._queries) do
query:Release()
end
wipe(self._queries)
self._queriesScanned = 0
self._queryDidBrowse = false
self._onProgressUpdateHandler = nil
self._onQueryDoneHandler = nil
wipe(self._resultsUpdateCallbacks)
self._nextSearchItemFunction = nil
self._currentSearchChangedCallback = nil
self._cancelled = false
self._shouldPause = false
self._paused = false
wipe(self._findResult)
self._scanQuery = nil
if self._findQuery then
self._findQuery:Release()
self._findQuery = nil
end
self._numItems = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AuctionScanManager.Release(self)
self:_Release()
private.objectPool:Recycle(self)
end
function AuctionScanManager.SetResolveSellers(self, resolveSellers)
self._resolveSellers = resolveSellers
return self
end
function AuctionScanManager.SetIgnoreItemLevel(self, ignoreItemLevel)
self._ignoreItemLevel = ignoreItemLevel
return self
end
function AuctionScanManager.SetScript(self, script, handler)
if script == "OnProgressUpdate" then
self._onProgressUpdateHandler = handler
elseif script == "OnQueryDone" then
self._onQueryDoneHandler = handler
elseif script == "OnCurrentSearchChanged" then
self._currentSearchChangedCallback = handler
else
error("Unknown AuctionScanManager script: "..tostring(script))
end
return self
end
function AuctionScanManager.AddResultsUpdateCallback(self, func)
self._resultsUpdateCallbacks[func] = true
end
function AuctionScanManager.RemoveResultsUpdateCallback(self, func)
self._resultsUpdateCallbacks[func] = nil
end
function AuctionScanManager.SetNextSearchItemFunction(self, func)
self._nextSearchItemFunction = func
end
function AuctionScanManager.GetNumQueries(self)
return #self._queries
end
function AuctionScanManager.QueryIterator(self, offset)
return private.QueryIteratorHelper, self._queries, offset or 0
end
function AuctionScanManager.NewQuery(self)
local query = Query.Get()
self:_AddQuery(query)
return query
end
function AuctionScanManager.AddItemListQueriesThreaded(self, itemList)
assert(Threading.IsThreadContext())
-- remove duplicates
local usedItems = TempTable.Acquire()
for i = #itemList, 1, -1 do
local itemString = itemList[i]
if usedItems[itemString] then
tremove(itemList, i)
end
usedItems[itemString] = true
end
TempTable.Release(usedItems)
self._numItems = #itemList
QueryUtil.GenerateThreaded(itemList, private.NewQueryCallback, self)
end
function AuctionScanManager.ScanQueriesThreaded(self)
assert(Threading.IsThreadContext())
self._queriesScanned = 0
self._cancelled = false
AuctionHouseWrapper.GetAndResetTotalHookedTime()
self:_NotifyProgressUpdate()
-- loop through each filter to perform
local allSuccess = true
while self._queriesScanned < #self._queries do
local query = self._queries[self._queriesScanned + 1]
-- run the browse query
local querySuccess, numNewResults = self:_ProcessQuery(query)
if not querySuccess then
allSuccess = false
break
end
self._queriesScanned = self._queriesScanned + 1
self:_NotifyProgressUpdate()
if self._onQueryDoneHandler then
self:_onQueryDoneHandler(query, numNewResults)
end
self:_Pause()
end
if allSuccess then
local hookedTime, topAddon, topTime = AuctionHouseWrapper.GetAndResetTotalHookedTime()
if hookedTime > 1 and topAddon ~= "Blizzard_AuctionHouseUI" then
Log.PrintfUser(L["Scan was slowed down by %s seconds by other AH addons (%s seconds by %s)."], Math.Round(hookedTime, 0.1), Math.Round(topTime, 0.1), topAddon)
end
end
return allSuccess
end
function AuctionScanManager.FindAuctionThreaded(self, findSubRow, noSeller)
assert(Threading.IsThreadContext())
wipe(self._findResult)
if TSM.IsWowClassic() then
return self:_FindAuctionThreaded(findSubRow, noSeller)
else
return self:_FindAuctionThreaded83(findSubRow, noSeller)
end
end
function AuctionScanManager.PrepareForBidOrBuyout(self, index, subRow, noSeller, quantity, itemBuyout)
if TSM.IsWowClassic() then
return subRow:EqualsIndex(index, noSeller)
else
local itemString = subRow:GetItemString()
if ItemInfo.IsCommodity(itemString) then
local future = AuctionHouseWrapper.StartCommoditiesPurchase(ItemString.ToId(itemString), quantity, itemBuyout)
if not future then
return false
end
return true, future
else
return true
end
end
end
function AuctionScanManager.PlaceBidOrBuyout(self, index, bidBuyout, subRow, quantity)
if TSM.IsWowClassic() then
PlaceAuctionBid("list", index, bidBuyout)
return true
else
local itemString = subRow:GetItemString()
local future = nil
if ItemInfo.IsCommodity(itemString) then
local itemId = ItemString.ToId(itemString)
future = AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity)
else
local _, auctionId = subRow:GetListingInfo()
future = AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout)
quantity = 1
end
if not future then
return false
end
-- TODO: return this future and record the buyout once the future is done
future:Cancel()
return true
end
end
function AuctionScanManager.GetProgress(self)
local numQueries = self:GetNumQueries()
if self._queriesScanned == numQueries then
return 1
end
local currentQuery = self._queries[self._queriesScanned + 1]
local searchProgress = nil
if not self._queryDidBrowse or TSM.IsWowClassic() then
searchProgress = 0
else
searchProgress = currentQuery:GetSearchProgress() * (1 - BROWSE_PROGRESS) + BROWSE_PROGRESS
end
local queryStep = 1 / numQueries
local progress = min((self._queriesScanned + searchProgress) * queryStep, 1)
return progress, self._paused
end
function AuctionScanManager.Cancel(self)
self._cancelled = true
if self._scanQuery then
self._scanQuery:CancelBrowseOrSearch()
self._scanQuery = nil
end
end
function AuctionScanManager.SetPaused(self, paused)
self._shouldPause = paused
if self._scanQuery then
self._scanQuery:CancelBrowseOrSearch()
self._scanQuery = nil
end
end
function AuctionScanManager.GetNumItems(self)
return self._numItems
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function AuctionScanManager._AddQuery(self, query)
query:SetResolveSellers(self._resolveSellers)
query:SetCallback(self._queryCallback)
tinsert(self._queries, query)
end
function AuctionScanManager._IsCancelled(self)
return self._cancelled
end
function AuctionScanManager._Pause(self)
if not self._shouldPause then
return
end
self._paused = true
self:_NotifyProgressUpdate()
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback()
end
while self._shouldPause do
Threading.Yield(true)
end
self._paused = false
self:_NotifyProgressUpdate()
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback()
end
end
function AuctionScanManager._NotifyProgressUpdate(self)
if self._onProgressUpdateHandler then
self:_onProgressUpdateHandler()
end
end
function AuctionScanManager._ProcessQuery(self, query)
local prevMaxBrowseId = 0
for _, row in query:BrowseResultsIterator() do
prevMaxBrowseId = max(prevMaxBrowseId, row:GetMinBrowseId())
end
-- run the browse query
self._queryDidBrowse = false
while not self:_DoBrowse(query) do
if self._shouldPause then
-- this browse failed due to a pause request, so try again after we're resumed
self:_Pause()
-- wipe the browse results since we're going to do another search
query:WipeBrowseResults()
else
return false, 0
end
end
self._queryDidBrowse = true
self:_NotifyProgressUpdate()
local numNewResults = 0
if TSM.IsWowClassic() then
for _, row in query:BrowseResultsIterator() do
if row:GetMinBrowseId() > prevMaxBrowseId then
numNewResults = numNewResults + row:GetNumSubRows()
end
end
return true, numNewResults
end
local rows = Threading.AcquireSafeTempTable()
for baseItemString, row in query:BrowseResultsIterator() do
rows[baseItemString] = row
end
while true do
local baseItemString, row = nil, nil
if self._nextSearchItemFunction then
baseItemString = self._nextSearchItemFunction()
row = baseItemString and rows[baseItemString]
end
if not row then
baseItemString, row = next(rows)
end
if not row then
break
end
rows[baseItemString] = nil
if self._currentSearchChangedCallback then
self:_currentSearchChangedCallback(baseItemString)
end
-- store all the existing auctionIds so we can see what changed
local prevAuctionIds = Threading.AcquireSafeTempTable()
for _, subRow in row:SubRowIterator() do
local _, auctionId = subRow:GetListingInfo()
assert(not prevAuctionIds[auctionId])
prevAuctionIds[auctionId] = true
end
-- send the query for this item
while not self:_DoSearch(query, row) do
if self._shouldPause then
-- this search failed due to a pause request, so try again after we're resumed
self:_Pause()
-- wipe the search results since we're going to do another search
row:WipeSearchResults()
else
Threading.ReleaseSafeTempTable(prevAuctionIds)
Threading.ReleaseSafeTempTable(rows)
return false, numNewResults
end
end
local numSubRows = row:GetNumSubRows()
for _, subRow in row:SubRowIterator() do
local _, auctionId = subRow:GetListingInfo()
if not prevAuctionIds[auctionId] then
numNewResults = numNewResults + 1
end
end
Threading.ReleaseSafeTempTable(prevAuctionIds)
if numSubRows == 0 then
-- remove this row since there are no search results
query:RemoveResultRow(row)
end
self:_NotifyProgressUpdate()
self:_Pause()
Threading.Yield()
end
Threading.ReleaseSafeTempTable(rows)
return true, numNewResults
end
function AuctionScanManager._DoBrowse(self, query, ...)
return self:_DoBrowseSearchHelper(query, query:Browse(...))
end
function AuctionScanManager._DoSearch(self, query, ...)
return self:_DoBrowseSearchHelper(query, query:Search(...))
end
function AuctionScanManager._DoBrowseSearchHelper(self, query, future)
if not future then
return false
end
self._scanQuery = query
local result = Threading.WaitForFuture(future)
self._scanQuery = nil
Threading.Yield()
return result
end
function AuctionScanManager._FindAuctionThreaded(self, row, noSeller)
self._cancelled = false
-- make sure we're not in the middle of a query where the results are going to change on us
Threading.WaitForFunction(CanSendAuctionQuery)
-- search the current page for the auction
if self:_FindAuctionOnCurrentPage(row, noSeller) then
Log.Info("Found on current page")
return self._findResult
end
-- search for the item
local page, maxPage = 0, nil
while true do
-- query the AH
if self._findQuery then
self._findQuery:Release()
end
local itemString = row:GetItemString()
local level = ItemInfo.GetMinLevel(itemString)
local quality = ItemInfo.GetQuality(itemString)
assert(level and quality)
self._findQuery = Query.Get()
:SetStr(ItemInfo.GetName(itemString), true)
:SetQualityRange(quality, quality)
:SetLevelRange(level, level)
:SetClass(ItemInfo.GetClassId(itemString), ItemInfo.GetSubClassId(itemString))
:SetItems(itemString)
:SetResolveSellers(not noSeller)
:SetPage(page)
local filterSuccess = self:_DoBrowse(self._findQuery)
if self._findQuery then
self._findQuery:Release()
self._findQuery = nil
end
if not filterSuccess then
break
end
-- search this page for the row
if self:_FindAuctionOnCurrentPage(row, noSeller) then
Log.Info("Found auction (%d)", page)
return self._findResult
elseif self:_IsCancelled() then
break
end
local numPages = ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE)
local canBeLater = private.FindAuctionCanBeOnLaterPage(row)
maxPage = maxPage or numPages - 1
if not canBeLater and page < maxPage then
maxPage = page
end
if canBeLater and page < maxPage then
Log.Info("Trying next page (%d)", page + 1)
page = page + 1
else
return
end
end
end
function AuctionScanManager._FindAuctionOnCurrentPage(self, subRow, noSeller)
local found = false
for i = 1, GetNumAuctionItems("list") do
if subRow:EqualsIndex(i, noSeller) then
tinsert(self._findResult, i)
found = true
end
end
return found
end
function AuctionScanManager._FindAuctionThreaded83(self, findSubRow, noSeller)
assert(findSubRow:IsSubRow())
self._cancelled = false
noSeller = noSeller or findSubRow:IsCommodity()
local row = findSubRow:GetResultRow()
local findHash, findHashNoSeller = findSubRow:GetHashes()
if not self:_DoSearch(row:GetQuery(), row, false) then
return nil
end
local result = nil
-- first try to find a subRow with a full matching hash
for _, subRow in row:SubRowIterator() do
local quantity, numAuctions = subRow:GetQuantities()
local hash = subRow:GetHashes()
if hash == findHash then
result = (result or 0) + quantity * numAuctions
end
end
if result then
return result
end
-- next try to find the first subRow with a matching no-seller hash
local firstHash = nil
for _, subRow in row:SubRowIterator() do
local quantity, numAuctions = subRow:GetQuantities()
local hash, hashNoSeller = subRow:GetHashes()
if (not firstHash or hash == firstHash) and hashNoSeller == findHashNoSeller then
firstHash = hash
result = (result or 0) + quantity * numAuctions
end
end
return result
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.NewQueryCallback(query, self)
self:_AddQuery(query)
end
function private.FindAuctionCanBeOnLaterPage(row)
local pageAuctions = GetNumAuctionItems("list")
if pageAuctions == 0 then
-- there are no auctions on this page, so it cannot be on a later one
return false
end
local _, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", pageAuctions)
local itemBuyout = (buyout > 0) and floor(buyout / stackSize) or 0
local _, rowItemBuyout = row:GetBuyouts()
if rowItemBuyout > itemBuyout then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowItemBuyout < itemBuyout then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
local rowStackSize = row:GetQuantities()
if rowStackSize > stackSize then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowStackSize < stackSize then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
seller = private.FixSellerName(seller, sellerFull) or "?"
local rowSeller = row:GetOwnerInfo()
if rowSeller > seller then
-- item must be on a later page since it would be sorted after the last auction on this page
return true
elseif rowSeller < seller then
-- item cannot be on a later page since it would be sorted before the last auction on this page
return false
end
-- all the things we are sorting on are the same, so the auction could be on a later page
return true
end
function private.FixSellerName(seller, sellerFull)
local realm = GetRealmName()
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
return sellerFull
else
return seller
end
end
function private.QueryIteratorHelper(tbl, index)
index = index + 1
if index > #tbl then
return
end
return index, tbl[index]
end

View File

@@ -0,0 +1,511 @@
-- ------------------------------------------------------------------------------ --
-- 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

View File

@@ -0,0 +1,38 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Util = TSM.Init("Service.AuctionScanClasses.Util")
local ItemInfo = TSM.Include("Service.ItemInfo")
-- ============================================================================
-- Module Functions
-- ============================================================================
function Util.HasItemInfo(itemString)
local itemName = ItemInfo.GetName(itemString)
local itemLevel = ItemInfo.GetItemLevel(itemString)
local quality = ItemInfo.GetQuality(itemString)
local minLevel = ItemInfo.GetMinLevel(itemString)
local hasIsCommodity = TSM.IsWowClassic() or ItemInfo.IsCommodity(itemString) ~= nil
local hasCanHaveVariations = ItemInfo.CanHaveVariations(itemString) ~= nil
local result = itemName and itemLevel and quality and minLevel and hasIsCommodity and hasCanHaveVariations
if not result then
ItemInfo.FetchInfo(itemString)
end
return result
end
function Util.FixSellerName(seller, sellerFull)
local realm = GetRealmName()
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
return sellerFull
else
return seller
end
end