-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Buy = TSM.Vendoring:NewPackage("Buy") local Database = TSM.Include("Util.Database") local Delay = TSM.Include("Util.Delay") local Event = TSM.Include("Util.Event") local Log = TSM.Include("Util.Log") local TempTable = TSM.Include("Util.TempTable") local Theme = TSM.Include("Util.Theme") local ItemString = TSM.Include("Util.ItemString") local ItemInfo = TSM.Include("Service.ItemInfo") local Inventory = TSM.Include("Service.Inventory") local private = { merchantDB = nil, pendingIndex = nil, pendingQuantity = 0, } local FIRST_BUY_TIMEOUT = 5 local FIRST_BUY_TIMEOUT_PER_STACK = 1 local CONSECUTIVE_BUY_TIMEOUT = 5 -- ============================================================================ -- Module Functions -- ============================================================================ function Buy.OnInitialize() private.merchantDB = Database.NewSchema("MERCHANT") :AddUniqueNumberField("index") :AddStringField("itemString") :AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString") :AddNumberField("price") :AddStringField("costItemsText") :AddStringField("firstCostItemString") :AddNumberField("stackSize") :AddNumberField("numAvailable") :Commit() Event.Register("MERCHANT_SHOW", private.MerchantShowEventHandler) Event.Register("MERCHANT_CLOSED", private.MerchantClosedEventHandler) Event.Register("MERCHANT_UPDATE", private.MerchantUpdateEventHandler) Event.Register("CHAT_MSG_LOOT", private.ChatMsgLootEventHandler) end function Buy.CreateMerchantQuery() return private.merchantDB:NewQuery() end function Buy.NeedsRepair() local _, needsRepair = GetRepairAllCost() return needsRepair end function Buy.CanGuildRepair() return Buy.NeedsRepair() and not TSM.IsWowClassic() and CanGuildBankRepair() end function Buy.DoGuildRepair() RepairAllItems(true) end function Buy.DoRepair() RepairAllItems() end function Buy.GetMaxCanAfford(index) local maxCanAfford = math.huge local _, _, price, stackSize, _, _, _, extendedCost = GetMerchantItemInfo(index) local numAltCurrencies = GetMerchantItemCostInfo(index) -- bug with big keech vendor returning extendedCost = true for gold only items if numAltCurrencies == 0 then extendedCost = false end -- check the price if price > 0 then maxCanAfford = min(floor(GetMoney() / price), maxCanAfford) end -- check the extended cost if extendedCost then assert(numAltCurrencies > 0) for i = 1, numAltCurrencies do local _, costNum, costItemLink, currencyName = GetMerchantItemCostItem(index, i) local costItemString = ItemString.Get(costItemLink) local costNumHave = nil if costItemString then costNumHave = Inventory.GetBagQuantity(costItemString) + Inventory.GetBankQuantity(costItemString) + Inventory.GetReagentBankQuantity(costItemString) elseif currencyName then if TSM.IsShadowlands() then for j = 1, C_CurrencyInfo.GetCurrencyListSize() do local info = C_CurrencyInfo.GetCurrencyListInfo(j) if not info.isHeader and info.name == currencyName then costNumHave = info.quantity break end end else for j = 1, GetCurrencyListSize() do local name, isHeader, _, _, _, count = GetCurrencyListInfo(j) if not isHeader and name == currencyName then costNumHave = count break end end end end if costNumHave then maxCanAfford = min(floor(costNumHave / costNum), maxCanAfford) end end end return maxCanAfford * stackSize end function Buy.BuyItem(itemString, quantity) local index = private.GetFirstIndex(itemString) if not index then return end private.BuyIndex(index, quantity) end function Buy.BuyItemIndex(index, quantity) private.BuyIndex(index, quantity) end function Buy.CanBuyItem(itemString) local index = private.GetFirstIndex(itemString) return index and true or false end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.MerchantShowEventHandler() Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB) end function private.MerchantClosedEventHandler() private.ClearPendingContext() Delay.Cancel("UPDATE_MERCHANT_DB") Delay.Cancel("RESCAN_MERCHANT_DB") private.merchantDB:Truncate() end function private.MerchantUpdateEventHandler() Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB) end function private.UpdateMerchantDB() local needsRetry = false private.merchantDB:TruncateAndBulkInsertStart() for i = 1, GetMerchantNumItems() do local itemLink = GetMerchantItemLink(i) local itemString = ItemString.Get(itemLink) if itemString then ItemInfo.StoreItemInfoByLink(itemLink) local _, _, price, stackSize, numAvailable, _, _, extendedCost = GetMerchantItemInfo(i) local numAltCurrencies = GetMerchantItemCostInfo(i) -- bug with big keech vendor returning extendedCost = true for gold only items if numAltCurrencies == 0 then extendedCost = false end local costItemsText, firstCostItemString = "", "" if extendedCost then assert(numAltCurrencies > 0) local costItems = TempTable.Acquire() for j = 1, numAltCurrencies do local _, costNum, costItemLink = GetMerchantItemCostItem(i, j) local costItemString = ItemString.Get(costItemLink) local texture = nil if not costItemLink then needsRetry = true elseif costItemString then firstCostItemString = firstCostItemString ~= "" and firstCostItemString or costItemString texture = ItemInfo.GetTexture(costItemString) elseif strmatch(costItemLink, "currency:") then if TSM.IsShadowlands() then texture = C_CurrencyInfo.GetCurrencyInfoFromLink(costItemLink).iconFileID else _, _, texture = GetCurrencyInfo(costItemLink) end firstCostItemString = strmatch(costItemLink, "(currency:%d+)") else error(format("Unknown item cost (%d, %d, %s)", i, costNum, tostring(costItemLink))) end if TSM.Vendoring.Buy.GetMaxCanAfford(i) < stackSize then costNum = Theme.GetFeedbackColor("RED"):ColorText(costNum) end tinsert(costItems, costNum.." |T"..(texture or "")..":12|t") end costItemsText = table.concat(costItems, " ") TempTable.Release(costItems) end private.merchantDB:BulkInsertNewRow(i, itemString, price, costItemsText, firstCostItemString, stackSize, numAvailable) end end private.merchantDB:BulkInsertEnd() if needsRetry then Log.Err("Failed to scan merchant") Delay.AfterTime("RESCAN_MERCHANT_DB", 0.2, private.UpdateMerchantDB) else Delay.Cancel("RESCAN_MERCHANT_DB") end end function private.GetFirstIndex(itemString) local index = Buy.CreateMerchantQuery() :Equal("itemString", itemString) :OrderBy("index", true) :Select("index") :GetFirstResultAndRelease() if not index and ItemString.GetBaseFast(itemString) == itemString then index = Buy.CreateMerchantQuery() :Equal("baseItemString", itemString) :OrderBy("index", true) :Select("index") :GetFirstResultAndRelease() end return index end function private.BuyIndex(index, quantity) local maxStack = GetMerchantItemMaxStack(index) quantity = min(quantity, Buy.GetMaxCanAfford(index)) if quantity == 0 then return end private.ClearPendingContext() private.pendingIndex = index local numStacks = 0 while quantity > 0 do local buyQuantity = min(quantity, maxStack) BuyMerchantItem(index, buyQuantity) private.pendingQuantity = private.pendingQuantity + buyQuantity quantity = quantity - buyQuantity numStacks = numStacks + 1 end Log.Info("Buying %d of %d (%d stacks)", private.pendingQuantity, index, numStacks) Delay.AfterTime("VENDORING_BUY_TIMEOUT", numStacks * FIRST_BUY_TIMEOUT_PER_STACK + FIRST_BUY_TIMEOUT, private.BuyTimeout) end function private.ChatMsgLootEventHandler(_, msg) if not private.pendingIndex then return end local link = GetMerchantItemLink(private.pendingIndex) if not link then Log.Err("Failed to get link (%s)", private.pendingIndex) private.ClearPendingContext() return end local quantity = nil if msg == format(LOOT_ITEM_PUSHED_SELF, link) then quantity = 1 else for i = 1, GetMerchantItemMaxStack(private.pendingIndex) do if msg == format(LOOT_ITEM_PUSHED_SELF_MULTIPLE, link, i) then quantity = i break end end end Log.Info("Got CHAT_MSG_LOOT(%s) with a quantity of %s (%d pending)", msg, tostring(quantity), private.pendingQuantity) if not quantity then return end private.pendingQuantity = private.pendingQuantity - quantity if private.pendingQuantity <= 0 then -- we're done private.ClearPendingContext() return end -- reset the timeout Delay.Cancel("VENDORING_BUY_TIMEOUT") Delay.AfterTime("VENDORING_BUY_TIMEOUT", CONSECUTIVE_BUY_TIMEOUT, private.BuyTimeout) end function private.BuyTimeout() Log.Warn("Retrying buying (%d, %d)", private.pendingIndex, private.pendingQuantity) Buy.BuyItemIndex(private.pendingIndex, private.pendingQuantity) end function private.ClearPendingContext() private.pendingIndex = nil private.pendingQuantity = 0 Delay.Cancel("VENDORING_BUY_TIMEOUT") end