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