-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Gathering = TSM.Crafting:NewPackage("Gathering") local DisenchantInfo = TSM.Include("Data.DisenchantInfo") local Database = TSM.Include("Util.Database") local Table = TSM.Include("Util.Table") local Delay = TSM.Include("Util.Delay") local String = TSM.Include("Util.String") local TempTable = TSM.Include("Util.TempTable") local ItemInfo = TSM.Include("Service.ItemInfo") local Conversions = TSM.Include("Service.Conversions") local BagTracking = TSM.Include("Service.BagTracking") local Inventory = TSM.Include("Service.Inventory") local PlayerInfo = TSM.Include("Service.PlayerInfo") local private = { db = nil, queuedCraftsUpdateQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons crafterList = {}, professionList = {}, contextChangedCallback = nil, } -- ============================================================================ -- Module Functions -- ============================================================================ function Gathering.OnInitialize() if TSM.IsWowClassic() then Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "guildBank") Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "altGuildBank") end end function Gathering.OnEnable() private.db = Database.NewSchema("GATHERING_MATS") :AddUniqueStringField("itemString") :AddNumberField("numNeed") :AddNumberField("numHave") :AddStringField("sourcesStr") :Commit() private.queuedCraftsUpdateQuery = TSM.Crafting.CreateQueuedCraftsQuery() :SetUpdateCallback(private.OnQueuedCraftsUpdated) private.OnQueuedCraftsUpdated() BagTracking.RegisterCallback(function() Delay.AfterTime("GATHERING_BAG_UPDATE", 1, private.UpdateDB) end) end function Gathering.SetContextChangedCallback(callback) private.contextChangedCallback = callback end function Gathering.CreateQuery() return private.db:NewQuery() end function Gathering.SetCrafter(crafter) if crafter == TSM.db.factionrealm.gatheringContext.crafter then return end TSM.db.factionrealm.gatheringContext.crafter = crafter wipe(TSM.db.factionrealm.gatheringContext.professions) private.UpdateProfessionList() private.UpdateDB() end function Gathering.SetProfessions(professions) local numProfessions = Table.Count(TSM.db.factionrealm.gatheringContext.professions) local didChange = false if numProfessions ~= #professions then didChange = true else for _, profession in ipairs(professions) do if not TSM.db.factionrealm.gatheringContext.professions[profession] then didChange = true end end end if not didChange then return end wipe(TSM.db.factionrealm.gatheringContext.professions) for _, profession in ipairs(professions) do assert(private.professionList[profession]) TSM.db.factionrealm.gatheringContext.professions[profession] = true end private.UpdateDB() end function Gathering.GetCrafterList() return private.crafterList end function Gathering.GetCrafter() return TSM.db.factionrealm.gatheringContext.crafter ~= "" and TSM.db.factionrealm.gatheringContext.crafter or nil end function Gathering.GetProfessionList() return private.professionList end function Gathering.GetProfessions() return TSM.db.factionrealm.gatheringContext.professions end function Gathering.SourcesStrToTable(sourcesStr, info, alts) for source, num, characters in gmatch(sourcesStr, "([a-zA-Z]+)/([0-9]+)/([^,]*)") do info[source] = tonumber(num) if source == "alt" or source == "altGuildBank" then for character in gmatch(characters, "([^`]+)") do alts[character] = true end end end end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.UpdateCrafterList() local query = TSM.Crafting.CreateQueuedCraftsQuery() :Select("players") :Distinct("players") wipe(private.crafterList) for _, players in query:Iterator() do for character in gmatch(players, "[^,]+") do if not private.crafterList[character] then private.crafterList[character] = true tinsert(private.crafterList, character) end end end query:Release() if TSM.db.factionrealm.gatheringContext.crafter ~= "" and not private.crafterList[TSM.db.factionrealm.gatheringContext.crafter] then -- the crafter which was selected no longer exists, so clear the selection TSM.db.factionrealm.gatheringContext.crafter = "" elseif #private.crafterList == 1 then -- there is only one crafter in the list, so select it TSM.db.factionrealm.gatheringContext.crafter = private.crafterList[1] end if TSM.db.factionrealm.gatheringContext.crafter == "" then wipe(TSM.db.factionrealm.gatheringContext.professions) end end function private.UpdateProfessionList() -- update the professionList wipe(private.professionList) if TSM.db.factionrealm.gatheringContext.crafter ~= "" then -- populate the list of professions local query = TSM.Crafting.CreateQueuedCraftsQuery() :Select("profession") :Custom(private.QueryPlayerFilter, TSM.db.factionrealm.gatheringContext.crafter) :Distinct("profession") for _, profession in query:Iterator() do private.professionList[profession] = true tinsert(private.professionList, profession) end query:Release() end -- remove selected professions which are no longer in the list for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do if not private.professionList[profession] then TSM.db.factionrealm.gatheringContext.professions[profession] = nil end end -- select all professions by default if not next(TSM.db.factionrealm.gatheringContext.professions) then for _, profession in ipairs(private.professionList) do TSM.db.factionrealm.gatheringContext.professions[profession] = true end end end function private.OnQueuedCraftsUpdated() private.UpdateCrafterList() private.UpdateProfessionList() private.UpdateDB() private.contextChangedCallback() end function private.UpdateDB() -- delay the update if we're in combat if InCombatLockdown() then Delay.AfterTime("DELAYED_GATHERING_UPDATE", 1, private.UpdateDB) return end local crafter = TSM.db.factionrealm.gatheringContext.crafter if crafter == "" or not next(TSM.db.factionrealm.gatheringContext.professions) then private.db:Truncate() return end local matsNumNeed = TempTable.Acquire() local query = TSM.Crafting.CreateQueuedCraftsQuery() :Select("spellId", "num") :Custom(private.QueryPlayerFilter, crafter) :Or() for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do query:Equal("profession", profession) end query:End() for _, spellId, numQueued in query:Iterator() do for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do matsNumNeed[itemString] = (matsNumNeed[itemString] or 0) + quantity * numQueued end end query:Release() local matQueue = TempTable.Acquire() local matsNumHave = TempTable.Acquire() local matsNumHaveExtra = TempTable.Acquire() for itemString, numNeed in pairs(matsNumNeed) do matsNumHave[itemString] = private.GetCrafterInventoryQuantity(itemString) local numUsed = nil numNeed, numUsed = private.HandleNumHave(itemString, numNeed, matsNumHave[itemString]) if numUsed < matsNumHave[itemString] then matsNumHaveExtra[itemString] = matsNumHave[itemString] - numUsed end if numNeed > 0 then matsNumNeed[itemString] = numNeed tinsert(matQueue, itemString) else matsNumNeed[itemString] = nil end end local sourceList = TempTable.Acquire() local matSourceList = TempTable.Acquire() while #matQueue > 0 do local itemString = tremove(matQueue) wipe(sourceList) local numNeed = matsNumNeed[itemString] -- always add a task to get mail on the crafter if possible numNeed = private.ProcessSource(itemString, numNeed, "openMail", sourceList) assert(numNeed >= 0) for _, source in ipairs(TSM.db.profile.gatheringOptions.sources) do local isCraftSource = source == "craftProfit" or source == "craftNoProfit" local ignoreSource = false if isCraftSource then -- check if we are already crafting some materials of this craft so shouldn't craft this item local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true) if spellId then for _, matItemString in TSM.Crafting.MatIterator(spellId) do if not ignoreSource and matSourceList[matItemString] and strmatch(matSourceList[matItemString], "craft[a-zA-Z]+/[^,]+/") then ignoreSource = true end end else -- can't craft this item ignoreSource = true end end if not ignoreSource then local prevNumNeed = numNeed numNeed = private.ProcessSource(itemString, numNeed, source, sourceList) assert(numNeed >= 0) if numNeed == 0 then if isCraftSource then -- we are crafting these, so add the necessary mats local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true) assert(spellId) local numToCraft = ceil(prevNumNeed / TSM.Crafting.GetNumResult(spellId)) for _, intMatItemString, intMatQuantity in TSM.Crafting.MatIterator(spellId) do local intMatNumNeed, numUsed = private.HandleNumHave(intMatItemString, numToCraft * intMatQuantity, matsNumHaveExtra[intMatItemString] or 0) if numUsed > 0 then matsNumHaveExtra[intMatItemString] = matsNumHaveExtra[intMatItemString] - numUsed end if intMatNumNeed > 0 then if not matsNumNeed[intMatItemString] then local intMatNumHave = private.GetCrafterInventoryQuantity(intMatItemString) if intMatNumNeed > intMatNumHave then matsNumHave[intMatItemString] = intMatNumHave matsNumNeed[intMatItemString] = intMatNumNeed - intMatNumHave tinsert(matQueue, intMatItemString) elseif intMatNumHave > intMatNumNeed then matsNumHaveExtra[intMatItemString] = intMatNumHave - intMatNumNeed end else matsNumNeed[intMatItemString] = (matsNumNeed[intMatItemString] or 0) + intMatNumNeed if matSourceList[intMatItemString] then -- already processed this item, so queue it again tinsert(matQueue, intMatItemString) end end end end end break end end end sort(sourceList) matSourceList[itemString] = table.concat(sourceList, ",") end private.db:TruncateAndBulkInsertStart() for itemString, numNeed in pairs(matsNumNeed) do private.db:BulkInsertNewRow(itemString, numNeed, matsNumHave[itemString], matSourceList[itemString]) end private.db:BulkInsertEnd() TempTable.Release(sourceList) TempTable.Release(matSourceList) TempTable.Release(matsNumNeed) TempTable.Release(matsNumHave) TempTable.Release(matsNumHaveExtra) TempTable.Release(matQueue) end function private.ProcessSource(itemString, numNeed, source, sourceList) local crafter = TSM.db.factionrealm.gatheringContext.crafter local playerName = UnitName("player") if source == "openMail" then local crafterMailQuantity = Inventory.GetMailQuantity(itemString, crafter) if crafterMailQuantity > 0 then crafterMailQuantity = min(crafterMailQuantity, numNeed) if crafter == playerName then tinsert(sourceList, "openMail/"..crafterMailQuantity.."/") else tinsert(sourceList, "alt/"..crafterMailQuantity.."/"..crafter) end return numNeed - crafterMailQuantity end elseif source == "vendor" then if ItemInfo.GetVendorBuy(itemString) then -- assume we can buy all we need from the vendor tinsert(sourceList, "vendor/"..numNeed.."/") return 0 end elseif source == "guildBank" then local guild = PlayerInfo.GetPlayerGuild(crafter) local guildBankQuantity = guild and Inventory.GetGuildQuantity(itemString, guild) or 0 if guildBankQuantity > 0 then guildBankQuantity = min(guildBankQuantity, numNeed) if crafter == playerName then -- we are on the crafter tinsert(sourceList, "guildBank/"..guildBankQuantity.."/") else -- need to switch to the crafter to get items from the guild bank tinsert(sourceList, "altGuildBank/"..guildBankQuantity.."/"..crafter) end return numNeed - guildBankQuantity end elseif source == "alt" then if ItemInfo.IsSoulbound(itemString) then -- can't mail soulbound items return numNeed end if crafter ~= playerName then -- we are on the alt, so see if we can gather items from this character local bagQuantity = Inventory.GetBagQuantity(itemString) local bankQuantity = Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString) local mailQuantity = Inventory.GetMailQuantity(itemString) if bagQuantity > 0 then bagQuantity = min(numNeed, bagQuantity) tinsert(sourceList, "sendMail/"..bagQuantity.."/") numNeed = numNeed - bagQuantity if numNeed == 0 then return 0 end end if mailQuantity > 0 then mailQuantity = min(numNeed, mailQuantity) tinsert(sourceList, "openMail/"..mailQuantity.."/") numNeed = numNeed - mailQuantity if numNeed == 0 then return 0 end end if bankQuantity > 0 then bankQuantity = min(numNeed, bankQuantity) tinsert(sourceList, "bank/"..bankQuantity.."/") numNeed = numNeed - bankQuantity if numNeed == 0 then return 0 end end end -- check alts local altNum = 0 local altCharacters = TempTable.Acquire() for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do local characterKey = nil if factionrealm == UnitFactionGroup("player").." - "..GetRealmName() then characterKey = character else characterKey = character.." - "..factionrealm end if characterKey ~= crafter and characterKey ~= playerName then local num = 0 num = num + Inventory.GetBagQuantity(itemString, character, factionrealm) num = num + Inventory.GetBankQuantity(itemString, character, factionrealm) num = num + Inventory.GetReagentBankQuantity(itemString, character, factionrealm) num = num + Inventory.GetMailQuantity(itemString, character, factionrealm) if num > 0 then tinsert(altCharacters, characterKey) altNum = altNum + num end end end end local altCharactersStr = table.concat(altCharacters, "`") TempTable.Release(altCharacters) if altNum > 0 then altNum = min(altNum, numNeed) tinsert(sourceList, "alt/"..altNum.."/"..altCharactersStr) return numNeed - altNum end elseif source == "altGuildBank" then local currentGuild = PlayerInfo.GetPlayerGuild(playerName) if currentGuild and crafter ~= playerName then -- we are on an alt, so see if we can gather items from this character's guild bank local guildBankQuantity = Inventory.GetGuildQuantity(itemString) if guildBankQuantity > 0 then guildBankQuantity = min(numNeed, guildBankQuantity) tinsert(sourceList, "guildBank/"..guildBankQuantity.."/") numNeed = numNeed - guildBankQuantity if numNeed == 0 then return 0 end end end -- check alts local totalGuildBankQuantity = 0 local altCharacters = TempTable.Acquire() for _, character in PlayerInfo.CharacterIterator(true) do local guild = PlayerInfo.GetPlayerGuild(character) if guild and guild ~= currentGuild then local guildBankQuantity = Inventory.GetGuildQuantity(itemString, guild) if guildBankQuantity > 0 then tinsert(altCharacters, character) totalGuildBankQuantity = totalGuildBankQuantity + guildBankQuantity end end end local altCharactersStr = table.concat(altCharacters, "`") TempTable.Release(altCharacters) if totalGuildBankQuantity > 0 then totalGuildBankQuantity = min(totalGuildBankQuantity, numNeed) tinsert(sourceList, "altGuildBank/"..totalGuildBankQuantity.."/"..altCharactersStr) return numNeed - totalGuildBankQuantity end elseif source == "craftProfit" or source == "craftNoProfit" then local spellId, maxProfit = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true) if spellId and (source == "craftNoProfit" or (maxProfit and maxProfit > 0)) then -- assume we can craft all we need local numToCraft = ceil(numNeed / TSM.Crafting.GetNumResult(spellId)) tinsert(sourceList, source.."/"..numToCraft.."/") return 0 end elseif source == "auction" then if ItemInfo.IsSoulbound(itemString) then -- can't buy soulbound items return numNeed end -- assume we can buy all we need from the AH tinsert(sourceList, "auction/"..numNeed.."/") return 0 elseif source == "auctionCrafting" then if ItemInfo.IsSoulbound(itemString) then -- can't buy soulbound items return numNeed end if not Conversions.GetSourceItems(itemString) then -- can't convert to get this item return numNeed end -- assume we can buy all we need from the AH tinsert(sourceList, "auctionCrafting/"..numNeed.."/") return 0 elseif source == "auctionDE" then if ItemInfo.IsSoulbound(itemString) then -- can't buy soulbound items return numNeed end if not DisenchantInfo.IsTargetItem(itemString) then -- can't disenchant to get this item return numNeed end -- assume we can buy all we need from the AH tinsert(sourceList, "auctionDE/"..numNeed.."/") return 0 else error("Unkown source: "..tostring(source)) end return numNeed end function private.QueryPlayerFilter(row, player) return String.SeparatedContains(row:GetField("players"), ",", player) end function private.GetCrafterInventoryQuantity(itemString) local crafter = TSM.db.factionrealm.gatheringContext.crafter return Inventory.GetBagQuantity(itemString, crafter) + Inventory.GetReagentBankQuantity(itemString, crafter) + Inventory.GetBankQuantity(itemString, crafter) end function private.HandleNumHave(itemString, numNeed, numHave) if numNeed > numHave then -- use everything we have numNeed = numNeed - numHave return numNeed, numHave else -- we have at least as many as we need, so use all of them return 0, numNeed end end