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,776 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local Vararg = TSM.Include("Util.Vararg")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Conversions = TSM.Include("Service.Conversions")
local Inventory = TSM.Include("Service.Inventory")
local private = {
spellDB = nil,
matDB = nil,
matItemDB = nil,
matDBSpellIdQuery = nil,
matDBMatsInTableQuery = nil,
matDBMatNamesQuery = nil,
ignoredCooldownDB = nil,
}
local CHARACTER_KEY = UnitName("player").." - "..GetRealmName()
local IGNORED_COOLDOWN_SEP = "\001"
local PROFESSION_SEP = ","
local PLAYER_SEP = ","
local BAD_CRAFTING_PRICE_SOURCES = {
crafting = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
local used = TempTable.Acquire()
for _, craftInfo in pairs(TSM.db.factionrealm.internalData.crafts) do
for itemString in pairs(craftInfo.mats) do
if strmatch(itemString, "^o:") then
local _, _, matList = strsplit(":", itemString)
for matItemId in String.SplitIterator(matList, ",") do
used["i:"..matItemId] = true
end
else
used[itemString] = true
end
end
end
for itemString in pairs(used) do
TSM.db.factionrealm.internalData.mats[itemString] = TSM.db.factionrealm.internalData.mats[itemString] or {}
end
for itemString in pairs(TSM.db.factionrealm.internalData.mats) do
if not used[itemString] then
TSM.db.factionrealm.internalData.mats[itemString] = nil
end
end
TempTable.Release(used)
local professionItems = TempTable.Acquire()
local matSpellCount = TempTable.Acquire()
local matFirstItemString = TempTable.Acquire()
local matFirstQuantity = TempTable.Acquire()
private.matDB = Database.NewSchema("CRAFTING_MATS")
:AddNumberField("spellId")
:AddStringField("itemString")
:AddNumberField("quantity")
:AddIndex("spellId")
:AddIndex("itemString")
:Commit()
private.matDB:BulkInsertStart()
private.spellDB = Database.NewSchema("CRAFTING_SPELLS")
:AddUniqueNumberField("spellId")
:AddStringField("itemString")
:AddStringField("itemName")
:AddStringField("name")
:AddStringField("profession")
:AddNumberField("numResult")
:AddStringField("players")
:AddBooleanField("hasCD")
:AddIndex("itemString")
:Commit()
private.spellDB:BulkInsertStart()
local playersTemp = TempTable.Acquire()
for spellId, craftInfo in pairs(TSM.db.factionrealm.internalData.crafts) do
wipe(playersTemp)
for player in pairs(craftInfo.players) do
tinsert(playersTemp, player)
end
sort(playersTemp)
local playersStr = table.concat(playersTemp, PLAYER_SEP)
local itemName = ItemInfo.GetName(craftInfo.itemString) or ""
private.spellDB:BulkInsertNewRow(spellId, craftInfo.itemString, itemName, craftInfo.name or "", craftInfo.profession, craftInfo.numResult, playersStr, craftInfo.hasCD and true or false)
for matItemString, matQuantity in pairs(craftInfo.mats) do
private.matDB:BulkInsertNewRow(spellId, matItemString, matQuantity)
professionItems[craftInfo.profession] = professionItems[craftInfo.profession] or TempTable.Acquire()
matSpellCount[spellId] = (matSpellCount[spellId] or 0) + 1
if matQuantity > 0 then
matFirstItemString[spellId] = matItemString
matFirstQuantity[spellId] = matQuantity
end
if strmatch(matItemString, "^o:") then
local _, _, matList = strsplit(":", matItemString)
for matItemId in String.SplitIterator(matList, ",") do
local optionalMatItemString = "i:"..matItemId
professionItems[craftInfo.profession][optionalMatItemString] = true
end
else
professionItems[craftInfo.profession][matItemString] = true
end
end
end
TempTable.Release(playersTemp)
private.spellDB:BulkInsertEnd()
private.matDB:BulkInsertEnd()
private.matDBMatsInTableQuery = private.matDB:NewQuery()
:Select("itemString", "quantity")
:Equal("spellId", Database.BoundQueryParam())
:GreaterThan("quantity", 0)
private.matDBMatNamesQuery = private.matDB:NewQuery()
:Select("name")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:Equal("spellId", Database.BoundQueryParam())
:GreaterThan("quantity", 0)
private.matItemDB = Database.NewSchema("CRAFTING_MAT_ITEMS")
:AddUniqueStringField("itemString")
:AddStringField("professions")
:AddStringField("customValue")
:Commit()
private.matItemDB:BulkInsertStart()
local professionsTemp = TempTable.Acquire()
for itemString, info in pairs(TSM.db.factionrealm.internalData.mats) do
wipe(professionsTemp)
for profession, items in pairs(professionItems) do
if items[itemString] then
tinsert(professionsTemp, profession)
end
end
sort(professionsTemp)
local professionsStr = table.concat(professionsTemp)
private.matItemDB:BulkInsertNewRow(itemString, professionsStr, info.customValue or "")
end
TempTable.Release(professionsTemp)
private.matItemDB:BulkInsertEnd()
for _, tbl in pairs(professionItems) do
TempTable.Release(tbl)
end
TempTable.Release(professionItems)
private.matDBSpellIdQuery = private.matDB:NewQuery()
:Equal("spellId", Database.BoundQueryParam())
-- register 1:1 crafting conversions
local addedConversion = false
local query = private.spellDB:NewQuery()
:Select("spellId", "itemString", "numResult")
:Equal("hasCD", false)
for _, spellId, itemString, numResult in query:Iterator() do
if not ProfessionInfo.IsMassMill(spellId) and matSpellCount[spellId] == 1 then
Conversions.AddCraft(itemString, matFirstItemString[spellId], numResult / matFirstQuantity[spellId])
addedConversion = true
end
end
query:Release()
TempTable.Release(matSpellCount)
TempTable.Release(matFirstItemString)
TempTable.Release(matFirstQuantity)
if addedConversion then
CustomPrice.OnSourceChange("Destroy")
end
local isValid, err = CustomPrice.Validate(TSM.db.global.craftingOptions.defaultCraftPriceMethod, BAD_CRAFTING_PRICE_SOURCES)
if not isValid then
Log.PrintfUser(L["Your default craft value method was invalid so it has been returned to the default. Details: %s"], err)
TSM.db.global.craftingOptions.defaultCraftPriceMethod = TSM.db:GetDefault("global", "craftingOptions", "defaultCraftPriceMethod")
end
private.ignoredCooldownDB = Database.NewSchema("IGNORED_COOLDOWNS")
:AddStringField("characterKey")
:AddNumberField("spellId")
:Commit()
private.ignoredCooldownDB:BulkInsertStart()
for entry in pairs(TSM.db.factionrealm.userData.craftingCooldownIgnore) do
local characterKey, spellId = strsplit(IGNORED_COOLDOWN_SEP, entry)
spellId = tonumber(spellId)
if Crafting.HasSpellId(spellId) then
private.ignoredCooldownDB:BulkInsertNewRow(characterKey, spellId)
else
TSM.db.factionrealm.userData.craftingCooldownIgnore[entry] = nil
end
end
private.ignoredCooldownDB:BulkInsertEnd()
end
function Crafting.HasSpellId(spellId)
return private.spellDB:HasUniqueRow("spellId", spellId)
end
function Crafting.CreateRawCraftsQuery()
return private.spellDB:NewQuery()
end
function Crafting.CreateCraftsQuery()
return private.spellDB:NewQuery()
:LeftJoin(TSM.Crafting.Queue.GetDBForJoin(), "spellId")
:VirtualField("bagQuantity", "number", Inventory.GetBagQuantity, "itemString")
:VirtualField("auctionQuantity", "number", Inventory.GetAuctionQuantity, "itemString")
:VirtualField("craftingCost", "number", private.CraftingCostVirtualField, "spellId")
:VirtualField("itemValue", "number", private.ItemValueVirtualField, "itemString")
:VirtualField("profit", "number", private.ProfitVirtualField, "spellId")
:VirtualField("profitPct", "number", private.ProfitPctVirtualField, "spellId")
:VirtualField("saleRate", "number", private.SaleRateVirtualField, "itemString")
end
function Crafting.CreateQueuedCraftsQuery()
return private.spellDB:NewQuery()
:InnerJoin(TSM.Crafting.Queue.GetDBForJoin(), "spellId")
end
function Crafting.CreateCooldownSpellsQuery()
return private.spellDB:NewQuery()
:Equal("hasCD", true)
end
function Crafting.CreateRawMatItemQuery()
return private.matItemDB:NewQuery()
end
function Crafting.CreateMatItemQuery()
return private.matItemDB:NewQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:VirtualField("matCost", "number", private.MatCostVirtualField, "itemString")
:VirtualField("totalQuantity", "number", private.GetTotalQuantity, "itemString")
end
function Crafting.SpellIterator()
return private.spellDB:NewQuery()
:Select("spellId")
:IteratorAndRelease()
end
function Crafting.GetSpellIdsByItem(itemString)
local query = private.spellDB:NewQuery()
:Equal("itemString", itemString)
:Select("spellId", "hasCD")
return query:IteratorAndRelease()
end
function Crafting.GetMostProfitableSpellIdByItem(itemString, playerFilter, noCD)
local maxProfit, bestSpellId = nil, nil
local maxProfitCD, bestSpellIdCD = nil, nil
for _, spellId, hasCD in Crafting.GetSpellIdsByItem(itemString) do
if not playerFilter or playerFilter == "" or Vararg.In(playerFilter, Crafting.GetPlayers(spellId)) then
local profit = TSM.Crafting.Cost.GetProfitBySpellId(spellId)
if hasCD then
if profit and profit > (maxProfitCD or -math.huge) then
maxProfitCD = profit
bestSpellIdCD = spellId
elseif not maxProfitCD then
bestSpellIdCD = spellId
end
else
if profit and profit > (maxProfit or -math.huge) then
maxProfit = profit
bestSpellId = spellId
elseif not maxProfit then
bestSpellId = spellId
end
end
end
end
if noCD then
maxProfitCD = nil
bestSpellIdCD = nil
end
if maxProfit then
return bestSpellId, maxProfit
elseif maxProfitCD then
return bestSpellIdCD, maxProfitCD
else
return bestSpellId or bestSpellIdCD or nil, nil
end
end
function Crafting.GetItemString(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "itemString")
end
function Crafting.GetProfession(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "profession")
end
function Crafting.GetNumResult(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "numResult")
end
function Crafting.GetPlayers(spellId)
local players = private.spellDB:GetUniqueRowField("spellId", spellId, "players")
if not players then
return
end
return strsplit(PLAYER_SEP, players)
end
function Crafting.GetName(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "name")
end
function Crafting.MatIterator(spellId)
return private.matDB:NewQuery()
:Select("itemString", "quantity")
:Equal("spellId", spellId)
:GreaterThan("quantity", 0)
:IteratorAndRelease()
end
function Crafting.GetOptionalMatIterator(spellId)
return private.matDB:NewQuery()
:Select("itemString", "slotId", "text")
:VirtualField("slotId", "number", private.OptionalMatSlotIdVirtualField, "itemString")
:VirtualField("text", "string", private.OptionalMatTextVirtualField, "itemString")
:Equal("spellId", spellId)
:LessThan("quantity", 0)
:OrderBy("slotId", true)
:IteratorAndRelease()
end
function Crafting.GetMatsAsTable(spellId, tbl)
private.matDBMatsInTableQuery
:BindParams(spellId)
:AsTable(tbl)
end
function Crafting.RemovePlayers(spellId, playersToRemove)
local shouldRemove = TempTable.Acquire()
for _, player in ipairs(playersToRemove) do
shouldRemove[player] = true
end
local players = TempTable.Acquire(Crafting.GetPlayers(spellId))
for i = #players, 1, -1 do
local player = players[i]
if shouldRemove[player] then
TSM.db.factionrealm.internalData.crafts[spellId].players[player] = nil
tremove(players, i)
end
end
TempTable.Release(shouldRemove)
local query = private.spellDB:NewQuery()
:Equal("spellId", spellId)
local row = query:GetFirstResult()
local playersStr = strjoin(PLAYER_SEP, TempTable.UnpackAndRelease(players))
if playersStr ~= "" then
row:SetField("players", playersStr)
:Update()
query:Release()
return true
end
-- no more players so remove this spell and all its mats
private.spellDB:DeleteRow(row)
query:Release()
TSM.db.factionrealm.internalData.crafts[spellId] = nil
local removedMats = TempTable.Acquire()
private.matDB:SetQueryUpdatesPaused(true)
query = private.matDB:NewQuery()
:Equal("spellId", spellId)
for _, matRow in query:Iterator() do
removedMats[matRow:GetField("itemString")] = true
private.matDB:DeleteRow(matRow)
end
query:Release()
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
return false
end
function Crafting.RemovePlayerSpells(inactiveSpellIds)
local playerName = UnitName("player")
local query = private.spellDB:NewQuery()
:InTable("spellId", inactiveSpellIds)
:Custom(private.QueryPlayerFilter, playerName)
local removedSpellIds = TempTable.Acquire()
local toRemove = TempTable.Acquire()
private.spellDB:SetQueryUpdatesPaused(true)
if query:Count() > 0 then
Log.Info("Removing %d inactive spellds", query:Count())
end
for _, row in query:Iterator() do
local players = row:GetField("players")
if row:GetField("players") == playerName then
-- the current player was the only player, so we'll delete the entire row and all its mats
local spellId = row:GetField("spellId")
removedSpellIds[spellId] = true
TSM.db.factionrealm.internalData.crafts[spellId] = nil
tinsert(toRemove, row)
else
-- remove this player form the row
local playersTemp = TempTable.Acquire(strsplit(PLAYER_SEP, players))
assert(Table.RemoveByValue(playersTemp, playerName) == 1)
row:SetField("players", strjoin(PLAYER_SEP, TempTable.UnpackAndRelease(playersTemp)))
:Update()
end
end
for _, row in ipairs(toRemove) do
private.spellDB:DeleteRow(row)
end
TempTable.Release(toRemove)
query:Release()
private.spellDB:SetQueryUpdatesPaused(false)
local removedMats = TempTable.Acquire()
private.matDB:SetQueryUpdatesPaused(true)
local matQuery = private.matDB:NewQuery()
:InTable("spellId", removedSpellIds)
for _, matRow in matQuery:Iterator() do
removedMats[matRow:GetField("itemString")] = true
private.matDB:DeleteRow(matRow)
end
TempTable.Release(removedSpellIds)
matQuery:Release()
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
end
function Crafting.SetSpellDBQueryUpdatesPaused(paused)
private.spellDB:SetQueryUpdatesPaused(paused)
end
function Crafting.CreateOrUpdate(spellId, itemString, profession, name, numResult, player, hasCD)
local row = private.spellDB:GetUniqueRow("spellId", spellId)
if row then
local playersStr = row:GetField("players")
local foundPlayer = String.SeparatedContains(playersStr, PLAYER_SEP, player)
if not foundPlayer then
assert(playersStr ~= "")
playersStr = playersStr .. PLAYER_SEP .. player
end
row:SetField("itemString", itemString)
:SetField("profession", profession)
:SetField("itemName", ItemInfo.GetName(itemString) or "")
:SetField("name", name)
:SetField("numResult", numResult)
:SetField("players", playersStr)
:SetField("hasCD", hasCD)
:Update()
row:Release()
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
craftInfo.itemString = itemString
craftInfo.profession = profession
craftInfo.name = name
craftInfo.numResult = numResult
craftInfo.players[player] = true
craftInfo.hasCD = hasCD or nil
else
TSM.db.factionrealm.internalData.crafts[spellId] = {
mats = {},
players = { [player] = true },
queued = 0,
itemString = itemString,
name = name,
profession = profession,
numResult = numResult,
hasCD = hasCD,
}
private.spellDB:NewRow()
:SetField("spellId", spellId)
:SetField("itemString", itemString)
:SetField("profession", profession)
:SetField("itemName", ItemInfo.GetName(itemString) or "")
:SetField("name", name)
:SetField("numResult", numResult)
:SetField("players", player)
:SetField("hasCD", hasCD)
:Create()
end
end
function Crafting.AddPlayer(spellId, player)
if TSM.db.factionrealm.internalData.crafts[spellId].players[player] then
return
end
local row = private.spellDB:GetUniqueRow("spellId", spellId)
local playersStr = row:GetField("players")
assert(playersStr ~= "")
playersStr = playersStr .. PLAYER_SEP .. player
row:SetField("players", playersStr)
row:Update()
row:Release()
TSM.db.factionrealm.internalData.crafts[spellId].players[player] = true
end
function Crafting.SetMats(spellId, matQuantities)
if Table.Equal(TSM.db.factionrealm.internalData.crafts[spellId].mats, matQuantities) then
-- nothing changed
return
end
wipe(TSM.db.factionrealm.internalData.crafts[spellId].mats)
for itemString, quantity in pairs(matQuantities) do
TSM.db.factionrealm.internalData.crafts[spellId].mats[itemString] = quantity
end
private.matDB:SetQueryUpdatesPaused(true)
local removedMats = TempTable.Acquire()
local usedMats = TempTable.Acquire()
private.matDBSpellIdQuery:BindParams(spellId)
for _, row in private.matDBSpellIdQuery:Iterator() do
local itemString = row:GetField("itemString")
local quantity = matQuantities[itemString]
if not quantity then
-- remove this row
private.matDB:DeleteRow(row)
removedMats[itemString] = true
else
usedMats[itemString] = true
row:SetField("quantity", quantity)
:Update()
end
end
local profession = Crafting.GetProfession(spellId)
for itemString, quantity in pairs(matQuantities) do
if not usedMats[itemString] then
private.matDB:NewRow()
:SetField("spellId", spellId)
:SetField("itemString", itemString)
:SetField("quantity", quantity)
:Create()
if quantity > 0 then
private.MatItemDBUpdateOrInsert(itemString, profession)
else
local _, _, matList = strsplit(":", itemString)
for matItemId in String.SplitIterator(matList, ",") do
private.MatItemDBUpdateOrInsert("i:"..matItemId, profession)
end
end
end
end
TempTable.Release(usedMats)
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
end
function Crafting.SetMatCustomValue(itemString, value)
TSM.db.factionrealm.internalData.mats[itemString].customValue = value
private.matItemDB:GetUniqueRow("itemString", itemString)
:SetField("customValue", value or "")
:Update()
end
function Crafting.CanCraftItem(itemString)
local count = private.spellDB:NewQuery()
:Equal("itemString", itemString)
:CountAndRelease()
return count > 0
end
function Crafting.RestockHelp(link)
local itemString = ItemString.Get(link)
if not itemString then
Log.PrintUser(L["No item specified. Usage: /tsm restock_help [ITEM_LINK]"])
return
end
local msg = private.GetRestockHelpMessage(itemString)
Log.PrintfUser(L["Restock help for %s: %s"], link, msg)
end
function Crafting.IgnoreCooldown(spellId)
assert(not TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId])
TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId] = true
private.ignoredCooldownDB:NewRow()
:SetField("characterKey", CHARACTER_KEY)
:SetField("spellId", spellId)
:Create()
end
function Crafting.IsCooldownIgnored(spellId)
return TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId]
end
function Crafting.CreateIgnoredCooldownQuery()
return private.ignoredCooldownDB:NewQuery()
end
function Crafting.RemoveIgnoredCooldown(characterKey, spellId)
assert(TSM.db.factionrealm.userData.craftingCooldownIgnore[characterKey..IGNORED_COOLDOWN_SEP..spellId])
TSM.db.factionrealm.userData.craftingCooldownIgnore[characterKey..IGNORED_COOLDOWN_SEP..spellId] = nil
local row = private.ignoredCooldownDB:NewQuery()
:Equal("characterKey", characterKey)
:Equal("spellId", spellId)
:GetFirstResultAndRelease()
assert(row)
private.ignoredCooldownDB:DeleteRow(row)
row:Release()
end
function Crafting.GetMatNames(spellId)
return private.matDBMatNamesQuery:BindParams(spellId)
:JoinedString("name", "")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ProcessRemovedMats(removedMats)
private.matItemDB:SetQueryUpdatesPaused(true)
for itemString in pairs(removedMats) do
local numSpells = private.matDB:NewQuery()
:Equal("itemString", itemString)
:CountAndRelease()
if numSpells == 0 then
local matItemRow = private.matItemDB:GetUniqueRow("itemString", itemString)
private.matItemDB:DeleteRow(matItemRow)
matItemRow:Release()
end
end
private.matItemDB:SetQueryUpdatesPaused(false)
end
function private.CraftingCostVirtualField(spellId)
return TSM.Crafting.Cost.GetCraftingCostBySpellId(spellId) or Math.GetNan()
end
function private.ItemValueVirtualField(itemString)
return TSM.Crafting.Cost.GetCraftedItemValue(itemString) or Math.GetNan()
end
function private.ProfitVirtualField(spellId)
return TSM.Crafting.Cost.GetProfitBySpellId(spellId) or Math.GetNan()
end
function private.ProfitPctVirtualField(spellId)
local craftingCost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(spellId)
return (craftingCost and profit) and floor(profit * 100 / craftingCost) or Math.GetNan()
end
function private.SaleRateVirtualField(itemString)
local saleRate = TSM.AuctionDB.GetRegionItemData(itemString, "regionSalePercent")
return saleRate and (saleRate / 100) or Math.GetNan()
end
function private.MatCostVirtualField(itemString)
return TSM.Crafting.Cost.GetMatCost(itemString) or Math.GetNan()
end
function private.OptionalMatSlotIdVirtualField(matStr)
local _, slotId = strsplit(":", matStr)
return tonumber(slotId)
end
function private.OptionalMatTextVirtualField(matStr)
local _, _, matList = strsplit(":", matStr)
return TSM.Crafting.ProfessionUtil.GetOptionalMatText(matList) or OPTIONAL_REAGENT_POSTFIX
end
function private.GetRestockHelpMessage(itemString)
-- check if the item is in a group
local groupPath = TSM.Groups.GetPathByItem(itemString)
if not groupPath then
return L["This item is not in a TSM group."]
end
-- check that there's a crafting operation applied
if not TSM.Operations.Crafting.HasOperation(itemString) then
return format(L["There is no Crafting operation applied to this item's TSM group (%s)."], TSM.Groups.Path.Format(groupPath))
end
-- check if it's an invalid operation
local isValid, err = TSM.Operations.Crafting.IsValid(itemString)
if not isValid then
return err
end
-- check that this item is craftable
if not TSM.Crafting.CanCraftItem(itemString) then
return L["You don't know how to craft this item."]
end
-- check the restock quantity
local neededQuantity = TSM.Operations.Crafting.GetRestockQuantity(itemString, private.GetTotalQuantity(itemString))
if neededQuantity == 0 then
return L["You either already have at least your max restock quantity of this item or the number which would be queued is less than the min restock quantity."]
end
-- check if we would actually queue any
local cost, spellId = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
local numResult = spellId and TSM.Crafting.GetNumResult(spellId)
if neededQuantity < numResult then
return format(L["A single craft makes %d and you only need to restock %d."], numResult, neededQuantity)
end
-- check the prices on the item and the min profit
local hasMinProfit, minProfit = TSM.Operations.Crafting.GetMinProfit(itemString)
if hasMinProfit then
local craftedValue = TSM.Crafting.Cost.GetCraftedItemValue(itemString)
local profit = cost and craftedValue and (craftedValue - cost) or nil
-- check that there's a crafted value
if not craftedValue then
return L["The 'Craft Value Method' did not return a value for this item."]
end
-- check that there's a crafted cost
if not cost then
return L["This item does not have a crafting cost. Check that all of its mats have mat prices."]
end
-- check that there's a profit
assert(profit)
if not minProfit then
return L["The min profit did not evalulate to a valid value for this item."]
end
if profit < minProfit then
return format(L["The profit of this item (%s) is below the min profit (%s)."], Money.ToString(profit), Money.ToString(minProfit))
end
end
return L["This item will be added to the queue when you restock its group. If this isn't happening, please visit http://support.tradeskillmaster.com for further assistance."]
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.GetTotalQuantity(itemString)
return CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
end
function private.MatItemDBUpdateOrInsert(itemString, profession)
local matItemRow = private.matItemDB:GetUniqueRow("itemString", itemString)
if matItemRow then
-- update the professions if necessary
local professions = TempTable.Acquire(strsplit(PROFESSION_SEP, matItemRow:GetField("professions")))
if not Table.KeyByValue(professions, profession) then
tinsert(professions, profession)
sort(professions)
matItemRow:SetField("professions", table.concat(professions, PROFESSION_SEP))
:Update()
end
TempTable.Release(professions)
else
private.matItemDB:NewRow()
:SetField("itemString", itemString)
:SetField("professions", profession)
:SetField("customValue", TSM.db.factionrealm.internalData.mats[itemString].customValue or "")
:Create()
end
end

View File

@@ -0,0 +1,156 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Cost = TSM.Crafting:NewPackage("Cost")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local ItemString = TSM.Include("Util.ItemString")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
matsVisited = {},
matCostCache = {},
matsTemp = {},
matsTempInUse = false,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Cost.GetMatCost(itemString)
itemString = ItemString.GetBase(itemString)
if not TSM.db.factionrealm.internalData.mats[itemString] then
return
end
if private.matsVisited[itemString] then
-- there's a loop in the mat cost, so bail
return
end
local prevHash = private.matsVisited.hash
local hash = nil
if prevHash == nil then
-- this is a top-level mat, so just use the itemString as the hash
hash = itemString
else
if type(prevHash) == "string" then
-- this is a second-level mat where the previous hash is the itemString which needs to be hashed itself
prevHash = Math.CalculateHash(prevHash)
end
hash = Math.CalculateHash(itemString, prevHash)
end
private.matsVisited.hash = hash
private.matsVisited[itemString] = true
if private.matCostCache.lastUpdate ~= GetTime() then
wipe(private.matCostCache)
private.matCostCache.lastUpdate = GetTime()
end
if not private.matCostCache[hash] then
local priceStr = TSM.db.factionrealm.internalData.mats[itemString].customValue or TSM.db.global.craftingOptions.defaultMatCostMethod
private.matCostCache[hash] = CustomPrice.GetValue(priceStr, itemString)
end
private.matsVisited[itemString] = nil
private.matsVisited.hash = prevHash
return private.matCostCache[hash]
end
function Cost.GetCraftingCostBySpellId(spellId)
local cost = 0
local hasMats = false
local mats = nil
if private.matsTempInUse then
mats = TempTable.Acquire()
else
mats = private.matsTemp
private.matsTempInUse = true
wipe(mats)
end
TSM.Crafting.GetMatsAsTable(spellId, mats)
for itemString, quantity in pairs(mats) do
hasMats = true
local matCost = Cost.GetMatCost(itemString)
if not matCost then
cost = nil
elseif cost then
cost = cost + matCost * quantity
end
end
if mats == private.matsTemp then
private.matsTempInUse = false
else
TempTable.Release(mats)
end
if not cost or not hasMats then
return
end
cost = Math.Round(cost / TSM.Crafting.GetNumResult(spellId))
return cost > 0 and cost or nil
end
function Cost.GetCraftedItemValue(itemString)
local hasCraftPriceMethod, craftPrice = TSM.Operations.Crafting.GetCraftedItemValue(itemString)
if hasCraftPriceMethod then
return craftPrice
end
return CustomPrice.GetValue(TSM.db.global.craftingOptions.defaultCraftPriceMethod, itemString)
end
function Cost.GetProfitBySpellId(spellId)
local _, _, profit = Cost.GetCostsBySpellId(spellId)
return profit
end
function Cost.GetCostsBySpellId(spellId)
local craftingCost = Cost.GetCraftingCostBySpellId(spellId)
local itemString = TSM.Crafting.GetItemString(spellId)
local craftedItemValue = itemString and Cost.GetCraftedItemValue(itemString) or nil
return craftingCost, craftedItemValue, craftingCost and craftedItemValue and (craftedItemValue - craftingCost) or nil
end
function Cost.GetSaleRateBySpellId(spellId)
local itemString = TSM.Crafting.GetItemString(spellId)
return itemString and CustomPrice.GetItemPrice(itemString, "DBRegionSaleRate") or nil
end
function Cost.GetLowestCostByItem(itemString)
itemString = ItemString.GetBase(itemString)
local lowestCost, lowestSpellId = nil, nil
local cdCost, cdSpellId = nil, nil
local numSpells = 0
local singleSpellId = nil
for _, spellId, hasCD in TSM.Crafting.GetSpellIdsByItem(itemString) do
if not hasCD then
if singleSpellId == nil then
singleSpellId = spellId
elseif singleSpellId then
singleSpellId = 0
end
end
numSpells = numSpells + 1
local cost = Cost.GetCraftingCostBySpellId(spellId)
if cost and (not lowestCost or cost < lowestCost) then
-- exclude spells with cooldown if option to ignore is enabled and there is more than one way to craft
if hasCD then
cdCost = cost
cdSpellId = spellId
else
lowestCost = cost
lowestSpellId = spellId
end
end
end
if singleSpellId == 0 then
singleSpellId = nil
end
if numSpells == 1 and not lowestCost and cdCost then
-- only way to craft it is with a CD craft, so use that
lowestCost = cdCost
lowestSpellId = cdSpellId
end
return lowestCost, lowestSpellId or singleSpellId
end

View File

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

View File

@@ -0,0 +1,303 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local PlayerProfessions = TSM.Crafting:NewPackage("PlayerProfessions")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local Vararg = TSM.Include("Util.Vararg")
local Threading = TSM.Include("Service.Threading")
local private = {
playerProfessionsThread = nil,
playerProfessionsThreadRunning = false,
db = nil,
query = nil,
}
local TAILORING_ES = "Sastrería"
local TAILORING_SKILL_ES = "Costura"
local LEATHERWORKING_ES = "Peletería"
local LEATHERWORKING_SKILL_ES = "Marroquinería"
local ENGINEERING_FR = "Ingénieur"
local ENGINEERING_SKILL_FR = "Ingénierie"
local FIRST_AID_FR = "Premiers soins"
local FIRST_AID_SKILL_FR = "Secourisme"
-- ============================================================================
-- Module Functions
-- ============================================================================
function PlayerProfessions.OnInitialize()
private.db = Database.NewSchema("PLAYER_PROFESSIONS")
:AddStringField("player")
:AddStringField("profession")
:AddNumberField("skillId")
:AddNumberField("level")
:AddNumberField("maxLevel")
:AddBooleanField("isSecondary")
:AddIndex("player")
:Commit()
private.query = private.db:NewQuery()
:Select("player", "profession", "skillId", "level", "maxLevel")
:OrderBy("isSecondary", true)
:OrderBy("level", false)
:OrderBy("profession", true)
private.playerProfessionsThread = Threading.New("PLAYER_PROFESSIONS", private.PlayerProfessionsThread)
private.StartPlayerProfessionsThread()
Event.Register("SKILL_LINES_CHANGED", private.PlayerProfessionsSkillUpdate)
Event.Register("LEARNED_SPELL_IN_TAB", private.StartPlayerProfessionsThread)
end
function PlayerProfessions.CreateQuery()
return private.db:NewQuery()
end
function PlayerProfessions.Iterator()
return private.query:Iterator()
end
-- ============================================================================
-- Player Professions Thread
-- ============================================================================
function private.StartPlayerProfessionsThread()
if private.playerProfessionsThreadRunning then
Threading.Kill(private.playerProfessionsThread)
end
private.playerProfessionsThreadRunning = true
Threading.Start(private.playerProfessionsThread)
end
function private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, isSecondary)
local professionInfo = TSM.db.sync.internalData.playerProfessions[name] or {}
TSM.db.sync.internalData.playerProfessions[name] = professionInfo
-- preserve whether or not we've prompted to create groups and the profession link if possible
local oldPrompted = professionInfo.prompted or nil
local oldLink = professionInfo.link or nil
wipe(professionInfo)
professionInfo.skillId = skillId
professionInfo.level = level
professionInfo.maxLevel = maxLevel
professionInfo.isSecondary = isSecondary
professionInfo.prompted = oldPrompted
professionInfo.link = oldLink
end
function private.PlayerProfessionsSkillUpdate()
if TSM.IsWowClassic() then
local _, _, offset, numSpells = GetSpellTabInfo(1)
for i = offset + 1, offset + numSpells do
local name, subName = GetSpellBookItemName(i, BOOKTYPE_SPELL)
if not subName then
Delay.AfterTime(0.05, private.PlayerProfessionsSkillUpdate)
return
end
if name and subName and (ProfessionInfo.IsSubNameClassic(strtrim(subName, " ")) or name == ProfessionInfo.GetName("Smelting") or name == ProfessionInfo.GetName("Poisons") or name == LEATHERWORKING_ES or name == TAILORING_ES or name == ENGINEERING_FR or name == FIRST_AID_FR) and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then
local level, maxLevel = nil, nil
for j = 1, GetNumSkillLines() do
local skillName, _, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(j)
if skillName == name then
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ProfessionInfo.GetName("Smelting") and skillName == ProfessionInfo.GetName("Mining") then
name = ProfessionInfo.GetName("Mining")
level = skillRank
maxLevel = skillMaxRank
break
elseif name == LEATHERWORKING_ES and skillName == LEATHERWORKING_SKILL_ES then
name = LEATHERWORKING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == TAILORING_ES and skillName == TAILORING_SKILL_ES then
name = TAILORING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ENGINEERING_FR and skillName == ENGINEERING_SKILL_FR then
name = ENGINEERING_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
elseif name == FIRST_AID_FR and skillName == FIRST_AID_SKILL_FR then
name = FIRST_AID_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
end
end
if level and maxLevel and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
private.UpdatePlayerProfessionInfo(name, -1, level, maxLevel, name == GetSpellInfo(129))
end
end
end
else
local professionIds = TempTable.Acquire(GetProfessions())
for i, id in pairs(professionIds) do -- needs to be pairs since there might be holes
if id ~= 8 and id ~= 9 then -- ignore fishing and arheology
local name, _, level, maxLevel, _, _, skillId = GetProfessionInfo(id)
if not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, i > 2)
end
end
end
TempTable.Release(professionIds)
end
-- update our DB
private.db:TruncateAndBulkInsertStart()
for _, character in TSM.db:FactionrealmCharacterIterator() do
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "playerProfessions")
if playerProfessions then
for name, info in pairs(playerProfessions) do
private.db:BulkInsertNewRow(character, name, info.skillId or -1, info.level, info.maxLevel, info.isSecondary)
end
end
end
private.db:BulkInsertEnd()
end
function private.PlayerProfessionsThread()
-- get the player's tradeskills
if TSM.IsWowClassic() then
SpellBookFrame_UpdateSkillLineTabs()
else
SpellBook_UpdateProfTab()
end
local forgetProfession = Threading.AcquireSafeTempTable()
for name in pairs(TSM.db.sync.internalData.playerProfessions) do
forgetProfession[name] = true
end
if TSM.IsWowClassic() then
local _, _, offset, numSpells = GetSpellTabInfo(1)
for i = offset + 1, offset + numSpells do
local name, subName = GetSpellBookItemName(i, BOOKTYPE_SPELL)
if name and subName and (ProfessionInfo.IsSubNameClassic(strtrim(subName, " ")) or name == ProfessionInfo.GetName("Smelting") or name == ProfessionInfo.GetName("Poisons") or name == LEATHERWORKING_ES or name == TAILORING_ES or name == ENGINEERING_FR or name == FIRST_AID_FR) and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then
local level, maxLevel = nil, nil
for j = 1, GetNumSkillLines() do
local skillName, _, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(j)
if skillName == name then
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ProfessionInfo.GetName("Smelting") and skillName == ProfessionInfo.GetName("Mining") then
name = ProfessionInfo.GetName("Mining")
level = skillRank
maxLevel = skillMaxRank
break
elseif name == LEATHERWORKING_ES and skillName == LEATHERWORKING_SKILL_ES then
name = LEATHERWORKING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == TAILORING_ES and skillName == TAILORING_SKILL_ES then
name = TAILORING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ENGINEERING_FR and skillName == ENGINEERING_SKILL_FR then
name = ENGINEERING_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
elseif name == FIRST_AID_FR and skillName == FIRST_AID_SKILL_FR then
name = FIRST_AID_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
end
end
if level and maxLevel and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
forgetProfession[name] = nil
private.UpdatePlayerProfessionInfo(name, -1, level, maxLevel, name == GetSpellInfo(129))
end
end
end
else
Threading.WaitForFunction(GetProfessions)
local professionIds = Threading.AcquireSafeTempTable(GetProfessions())
-- ignore archeology and fishing which are in the 3rd and 4th slots respectively
professionIds[3] = nil
professionIds[4] = nil
for i, id in pairs(professionIds) do -- needs to be pairs since there might be holes
local name, _, level, maxLevel, _, _, skillId = Threading.WaitForFunction(GetProfessionInfo, id)
if not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
forgetProfession[name] = nil
private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, i > 2)
end
end
Threading.ReleaseSafeTempTable(professionIds)
end
for name in pairs(forgetProfession) do
TSM.db.sync.internalData.playerProfessions[name] = nil
end
Threading.ReleaseSafeTempTable(forgetProfession)
-- clean up crafts which are no longer known
local matUsed = Threading.AcquireSafeTempTable()
local spellIds = Threading.AcquireSafeTempTable()
for _, spellId in TSM.Crafting.SpellIterator() do
tinsert(spellIds, spellId)
end
for _, spellId in ipairs(spellIds) do
local playersToRemove = TempTable.Acquire()
for _, player in Vararg.Iterator(TSM.Crafting.GetPlayers(spellId)) do
-- check if the player still exists and still has this profession
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(player), "internalData", "playerProfessions")
if not playerProfessions or not playerProfessions[TSM.Crafting.GetProfession(spellId)] then
tinsert(playersToRemove, player)
end
end
local stillExists = true
if #playersToRemove > 0 then
stillExists = TSM.Crafting.RemovePlayers(spellId, playersToRemove)
end
TempTable.Release(playersToRemove)
if stillExists then
for _, itemString in TSM.Crafting.MatIterator(spellId) do
matUsed[itemString] = true
end
end
Threading.Yield()
end
Threading.ReleaseSafeTempTable(spellIds)
-- clean up mats which aren't used anymore
local toRemove = TempTable.Acquire()
for itemString, matInfo in pairs(TSM.db.factionrealm.internalData.mats) do
-- clear out old names
matInfo.name = nil
if not matUsed[itemString] then
tinsert(toRemove, itemString)
end
end
Threading.ReleaseSafeTempTable(matUsed)
for _, itemString in ipairs(toRemove) do
TSM.db.factionrealm.internalData.mats[itemString] = nil
end
TempTable.Release(toRemove)
-- update our DB
private.db:TruncateAndBulkInsertStart()
for _, character in TSM.db:FactionrealmCharacterIterator() do
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "playerProfessions")
if playerProfessions then
for name, info in pairs(playerProfessions) do
private.db:BulkInsertNewRow(character, name, info.skillId or -1, info.level, info.maxLevel, info.isSecondary)
end
end
end
private.db:BulkInsertEnd()
private.playerProfessionsThreadRunning = false
end

View File

@@ -0,0 +1,554 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionScanner = TSM.Crafting:NewPackage("ProfessionScanner")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
db = nil,
hasScanned = false,
callbacks = {},
disabled = false,
ignoreUpdatesUntil = 0,
optionalMatArrayTemp = { { itemID = nil, count = 1, index = nil } },
}
-- don't want to scan a bunch of times when the profession first loads so add a 10 frame debounce to update events
local SCAN_DEBOUNCE_FRAMES = 10
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionScanner.OnInitialize()
private.db = Database.NewSchema("CRAFTING_RECIPES")
:AddUniqueNumberField("index")
:AddUniqueNumberField("spellId")
:AddStringField("name")
:AddNumberField("categoryId")
:AddStringField("difficulty")
:AddNumberField("rank")
:AddNumberField("numSkillUps")
:Commit()
TSM.Crafting.ProfessionState.RegisterUpdateCallback(private.ProfessionStateUpdate)
if TSM.IsWowClassic() then
Event.Register("CRAFT_UPDATE", private.OnTradeSkillUpdateEvent)
Event.Register("TRADE_SKILL_UPDATE", private.OnTradeSkillUpdateEvent)
else
Event.Register("TRADE_SKILL_LIST_UPDATE", private.OnTradeSkillUpdateEvent)
end
Event.Register("CHAT_MSG_SKILL", private.ChatMsgSkillEventHandler)
end
function ProfessionScanner.SetDisabled(disabled)
if private.disabled == disabled then
return
end
private.disabled = disabled
if not disabled then
private.ScanProfession()
end
end
function ProfessionScanner.HasScanned()
return private.hasScanned
end
function ProfessionScanner.HasSkills()
return private.hasScanned and private.db:GetNumRows() > 0
end
function ProfessionScanner.RegisterHasScannedCallback(callback)
tinsert(private.callbacks, callback)
end
function ProfessionScanner.IgnoreNextProfessionUpdates()
private.ignoreUpdatesUntil = GetTime() + 1
end
function ProfessionScanner.CreateQuery()
return private.db:NewQuery()
end
function ProfessionScanner.GetIndexBySpellId(spellId)
assert(TSM.IsWowClassic() or private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "index")
end
function ProfessionScanner.GetCategoryIdBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "categoryId")
end
function ProfessionScanner.GetNameBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "name")
end
function ProfessionScanner.GetRankBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "rank")
end
function ProfessionScanner.GetNumSkillupsBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "numSkillUps")
end
function ProfessionScanner.GetDifficultyBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "difficulty")
end
function ProfessionScanner.GetFirstSpellId()
if not private.hasScanned then
return
end
return private.db:NewQuery()
:Select("spellId")
:OrderBy("index", true)
:GetFirstResultAndRelease()
end
function ProfessionScanner.HasSpellId(spellId)
return private.hasScanned and private.db:GetUniqueRowField("spellId", spellId, "index") and true or false
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
function private.ProfessionStateUpdate()
private.hasScanned = false
for _, callback in ipairs(private.callbacks) do
callback()
end
if TSM.Crafting.ProfessionState.GetCurrentProfession() then
private.db:Truncate()
private.OnTradeSkillUpdateEvent()
else
Delay.Cancel("PROFESSION_SCAN_DELAY")
end
end
function private.OnTradeSkillUpdateEvent()
Delay.Cancel("PROFESSION_SCAN_DELAY")
private.QueueProfessionScan()
end
function private.ChatMsgSkillEventHandler(_, msg)
local professionName = TSM.Crafting.ProfessionState.GetCurrentProfession()
if not professionName or not strmatch(msg, professionName) then
return
end
private.ignoreUpdatesUntil = 0
private.QueueProfessionScan()
end
-- ============================================================================
-- Profession Scanning
-- ============================================================================
function private.QueueProfessionScan()
Delay.AfterFrame("PROFESSION_SCAN_DELAY", SCAN_DEBOUNCE_FRAMES, private.ScanProfession)
end
function private.ScanProfession()
if InCombatLockdown() then
-- we are in combat, so try again in a bit
private.QueueProfessionScan()
return
elseif private.disabled then
return
elseif GetTime() < private.ignoreUpdatesUntil then
return
end
local professionName = TSM.Crafting.ProfessionState.GetCurrentProfession()
if not professionName then
-- profession hasn't fully opened yet
private.QueueProfessionScan()
return
end
assert(professionName and TSM.Crafting.ProfessionUtil.IsDataStable())
if TSM.IsWowClassic() then
-- TODO: check and clear filters on classic
else
local hadFilter = false
if C_TradeSkillUI.GetOnlyShowUnlearnedRecipes() then
C_TradeSkillUI.SetOnlyShowLearnedRecipes(true)
C_TradeSkillUI.SetOnlyShowUnlearnedRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.GetOnlyShowMakeableRecipes() then
C_TradeSkillUI.SetOnlyShowMakeableRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.GetOnlyShowSkillUpRecipes() then
C_TradeSkillUI.SetOnlyShowSkillUpRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.AnyRecipeCategoriesFiltered() then
C_TradeSkillUI.ClearRecipeCategoryFilter()
hadFilter = true
end
if C_TradeSkillUI.AreAnyInventorySlotsFiltered() then
C_TradeSkillUI.ClearInventorySlotFilter()
hadFilter = true
end
for i = 1, C_PetJournal.GetNumPetSources() do
if C_TradeSkillUI.IsAnyRecipeFromSource(i) and C_TradeSkillUI.IsRecipeSourceTypeFiltered(i) then
C_TradeSkillUI.ClearRecipeSourceTypeFilter()
hadFilter = true
break
end
end
if C_TradeSkillUI.GetRecipeItemNameFilter() ~= "" then
C_TradeSkillUI.SetRecipeItemNameFilter(nil)
hadFilter = true
end
local minItemLevel, maxItemLevel = C_TradeSkillUI.GetRecipeItemLevelFilter()
if minItemLevel ~= 0 or maxItemLevel ~= 0 then
C_TradeSkillUI.SetRecipeItemLevelFilter(0, 0)
hadFilter = true
end
if hadFilter then
-- an update event will be triggered
return
end
end
if TSM.IsWowClassic() then
local lastHeaderIndex = 0
private.db:TruncateAndBulkInsertStart()
for i = 1, TSM.Crafting.ProfessionState.IsClassicCrafting() and GetNumCrafts() or GetNumTradeSkills() do
local name, _, skillType, hash = nil, nil, nil, nil
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
name, _, skillType = GetCraftInfo(i)
if skillType ~= "header" then
hash = Math.CalculateHash(name)
for j = 1, GetCraftNumReagents(i) do
local _, _, quantity = GetCraftReagentInfo(i, j)
hash = Math.CalculateHash(ItemString.Get(GetCraftReagentItemLink(i, j)), hash)
hash = Math.CalculateHash(quantity, hash)
end
end
else
name, skillType = GetTradeSkillInfo(i)
if skillType ~= "header" then
hash = Math.CalculateHash(name)
for j = 1, GetTradeSkillNumReagents(i) do
local _, _, quantity = GetTradeSkillReagentInfo(i, j)
hash = Math.CalculateHash(ItemString.Get(GetTradeSkillReagentItemLink(i, j)), hash)
hash = Math.CalculateHash(quantity, hash)
end
end
end
if skillType == "header" then
lastHeaderIndex = i
else
if name then
private.db:BulkInsertNewRow(i, hash, name, lastHeaderIndex, skillType, -1, 1)
end
end
end
private.db:BulkInsertEnd()
else
local prevRecipeIds = TempTable.Acquire()
local nextRecipeIds = TempTable.Acquire()
local recipeLearned = TempTable.Acquire()
local recipes = TempTable.Acquire()
assert(C_TradeSkillUI.GetFilteredRecipeIDs(recipes) == recipes)
local spellIdIndex = TempTable.Acquire()
for index, spellId in ipairs(recipes) do
-- There's a Blizzard bug where First Aid duplicates spellIds, so check that we haven't seen this before
if not spellIdIndex[spellId] then
spellIdIndex[spellId] = index
local info = nil
if not TSM.IsShadowlands() then
info = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, info) == info)
else
info = C_TradeSkillUI.GetRecipeInfo(spellId)
end
if info.previousRecipeID then
prevRecipeIds[spellId] = info.previousRecipeID
nextRecipeIds[info.previousRecipeID] = spellId
end
if info.nextRecipeID then
nextRecipeIds[spellId] = info.nextRecipeID
prevRecipeIds[info.nextRecipeID] = spellId
end
recipeLearned[spellId] = info.learned
if not TSM.IsShadowlands() then
TempTable.Release(info)
end
end
end
private.db:TruncateAndBulkInsertStart()
local inactiveSpellIds = TempTable.Acquire()
for index, spellId in ipairs(recipes) do
local hasHigherRank = nextRecipeIds[spellId] and recipeLearned[nextRecipeIds[spellId]]
-- TODO: show unlearned recipes in the TSM UI
-- There's a Blizzard bug where First Aid duplicates spellIds, so check that this is the right index
if spellIdIndex[spellId] == index and recipeLearned[spellId] and not hasHigherRank then
local info = nil
if not TSM.IsShadowlands() then
info = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, info) == info)
else
info = C_TradeSkillUI.GetRecipeInfo(spellId)
end
local rank = -1
if prevRecipeIds[spellId] or nextRecipeIds[spellId] then
rank = 1
local tempSpellId = spellId
while prevRecipeIds[tempSpellId] do
rank = rank + 1
tempSpellId = prevRecipeIds[tempSpellId]
end
end
local numSkillUps = info.difficulty == "optimal" and info.numSkillUps or 1
private.db:BulkInsertNewRow(index, spellId, info.name, info.categoryID, info.difficulty, rank, numSkillUps)
if not TSM.IsShadowlands() then
TempTable.Release(info)
end
else
inactiveSpellIds[spellId] = true
end
end
private.db:BulkInsertEnd()
-- remove spells which are not active (i.e. older ranks)
if next(inactiveSpellIds) then
TSM.Crafting.RemovePlayerSpells(inactiveSpellIds)
end
TempTable.Release(inactiveSpellIds)
TempTable.Release(spellIdIndex)
TempTable.Release(recipes)
TempTable.Release(prevRecipeIds)
TempTable.Release(nextRecipeIds)
TempTable.Release(recipeLearned)
end
if TSM.Crafting.ProfessionUtil.IsNPCProfession() or TSM.Crafting.ProfessionUtil.IsLinkedProfession() or TSM.Crafting.ProfessionUtil.IsGuildProfession() then
-- we don't want to store this profession in our DB, so we're done
if not private.hasScanned then
private.hasScanned = true
for _, callback in ipairs(private.callbacks) do
callback()
end
end
return
end
if not TSM.db.sync.internalData.playerProfessions[professionName] then
-- we are in combat or the player's professions haven't been scanned yet by PlayerProfessions.lua, so try again in a bit
private.QueueProfessionScan()
return
end
-- update the link for this profession
TSM.db.sync.internalData.playerProfessions[professionName].link = not TSM.IsWowClassic() and C_TradeSkillUI.GetTradeSkillListLink() or nil
-- scan all the recipes
TSM.Crafting.SetSpellDBQueryUpdatesPaused(true)
local query = private.db:NewQuery()
:Select("spellId")
local numFailed = 0
for _, spellId in query:Iterator() do
if not private.ScanRecipe(professionName, spellId) then
numFailed = numFailed + 1
end
end
query:Release()
TSM.Crafting.SetSpellDBQueryUpdatesPaused(false)
Log.Info("Scanned %s (failed to scan %d)", professionName, numFailed)
if numFailed > 0 then
-- didn't completely scan, so we'll try again
private.QueueProfessionScan()
end
if not private.hasScanned then
private.hasScanned = true
for _, callback in ipairs(private.callbacks) do
callback()
end
end
-- explicitly run GC
collectgarbage()
end
function private.ScanRecipe(professionName, spellId)
-- get the links
local itemLink, lNum, hNum = TSM.Crafting.ProfessionUtil.GetRecipeInfo(TSM.IsWowClassic() and ProfessionScanner.GetIndexBySpellId(spellId) or spellId)
assert(itemLink, "Invalid craft: "..tostring(spellId))
-- get the itemString and craft name
local itemString, craftName = nil, nil
if strfind(itemLink, "enchant:") then
if TSM.IsWowClassic() then
return true
else
-- result of craft is not an item
itemString = ProfessionInfo.GetIndirectCraftResult(spellId)
if not itemString then
-- we don't care about this craft
return true
end
craftName = GetSpellInfo(spellId)
end
elseif strfind(itemLink, "item:") then
-- result of craft is item
itemString = ItemString.GetBase(itemLink)
craftName = ItemInfo.GetName(itemLink)
-- Blizzard broke Brilliant Scarlet Ruby in 8.3, so just hard-code a workaround
if spellId == 53946 and not itemString and not craftName then
itemString = "i:39998"
craftName = GetSpellInfo(spellId)
end
else
error("Invalid craft: "..tostring(spellId))
end
if not itemString or not craftName then
Log.Warn("No itemString (%s) or craftName (%s) found (%s, %s)", tostring(itemString), tostring(craftName), tostring(professionName), tostring(spellId))
return false
end
-- get the result number
local numResult = nil
local isEnchant = professionName == GetSpellInfo(7411) and strfind(itemLink, "enchant:")
if isEnchant then
numResult = 1
else
-- workaround for incorrect values returned for Temporal Crystal
if spellId == 169092 and itemString == "i:113588" then
lNum, hNum = 1, 1
end
-- workaround for incorrect values returned for new mass milling recipes
if ProfessionInfo.IsMassMill(spellId) then
if spellId == 210116 then -- Yseralline
lNum, hNum = 4, 4 -- always four
elseif spellId == 209664 then -- Felwort
lNum, hNum = 42, 42 -- amount is variable but the values are conservative
elseif spellId == 247861 then -- Astral Glory
lNum, hNum = 4, 4 -- amount is variable but the values are conservative
else
lNum, hNum = 8, 8.8
end
end
numResult = floor(((lNum or 1) + (hNum or 1)) / 2)
end
-- store general info about this recipe
local hasCD = TSM.Crafting.ProfessionUtil.HasCooldown(spellId)
TSM.Crafting.CreateOrUpdate(spellId, itemString, professionName, craftName, numResult, UnitName("player"), hasCD)
-- get the mat quantities and add mats to our DB
local matQuantities = TempTable.Acquire()
local haveInvalidMats = false
local numReagents = TSM.Crafting.ProfessionUtil.GetNumMats(spellId)
for i = 1, numReagents do
local matItemLink, name, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(spellId, i)
local matItemString = ItemString.GetBase(matItemLink)
if not matItemString then
Log.Warn("Failed to get itemString for mat %d (%s, %s)", i, tostring(professionName), tostring(spellId))
haveInvalidMats = true
break
end
if not name or not quantity then
Log.Warn("Failed to get name (%s) or quantity (%s) for mat (%s, %s, %d)", tostring(name), tostring(quantity), tostring(professionName), tostring(spellId), i)
haveInvalidMats = true
break
end
ItemInfo.StoreItemName(matItemString, name)
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
matQuantities[matItemString] = quantity
end
-- if this is an enchant, add a vellum to the list of mats
if isEnchant then
local matItemString = ProfessionInfo.GetVellumItemString()
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
matQuantities[matItemString] = 1
end
if not haveInvalidMats then
local optionalMats = private.GetOptionalMats(spellId)
if optionalMats then
for _, matStr in ipairs(optionalMats) do
local _, _, mats = strsplit(":", matStr)
for itemId in String.SplitIterator(mats, ",") do
local matItemString = "i:"..itemId
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
end
matQuantities[matStr] = -1
end
end
TSM.Crafting.SetMats(spellId, matQuantities)
end
TempTable.Release(matQuantities)
return not haveInvalidMats
end
function private.GetOptionalMats(spellId)
local optionalMats = TSM.IsShadowlands() and C_TradeSkillUI.GetOptionalReagentInfo(spellId) or nil
if not optionalMats or #optionalMats == 0 then
return nil
end
for i, info in ipairs(optionalMats) do
if info.requiredSkillRank ~= 0 then
-- TODO: handle this case
return nil
else
-- process the options
assert(#info.options > 0)
-- sort the optional mats by itemId
sort(info.options)
-- cache the optional mat info
for _, itemId in ipairs(info.options) do
assert(type(itemId) == "number")
private.CacheOptionalMatInfo(spellId, i, itemId)
end
local matList = table.concat(info.options, ",")
TSM.Crafting.ProfessionUtil.StoreOptionalMatText(matList, info.slotText)
optionalMats[i] = "o:"..i..":"..matList
end
end
return optionalMats
end
function private.CacheOptionalMatInfo(spellId, index, itemId)
if TSM.db.global.internalData.optionalMatBonusIdLookup[itemId] then
return
end
if not TSMScanTooltip then
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
end
private.optionalMatArrayTemp.itemID = itemId
private.optionalMatArrayTemp.slot = index
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
TSMScanTooltip:ClearLines()
TSMScanTooltip:SetRecipeResultItem(spellId, private.optionalMatArrayTemp)
local _, itemLink = TSMScanTooltip:GetItem()
local bonusId = strmatch(itemLink, "item:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:2:3524:([0-9]+)")
TSM.db.global.internalData.optionalMatBonusIdLookup[itemId] = tonumber(bonusId)
end

View File

@@ -0,0 +1,191 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionState = TSM.Crafting:NewPackage("ProfessionState")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local FSM = TSM.Include("Util.FSM")
local Log = TSM.Include("Util.Log")
local private = {
fsm = nil,
updateCallbacks = {},
isClosed = true,
craftOpen = nil,
tradeSkillOpen = nil,
professionName = nil,
}
local WAIT_FRAME_DELAY = 5
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionState.OnInitialize()
private.CreateFSM()
end
function ProfessionState.RegisterUpdateCallback(callback)
tinsert(private.updateCallbacks, callback)
end
function ProfessionState.GetIsClosed()
return private.isClosed
end
function ProfessionState.IsClassicCrafting()
return TSM.IsWowClassic() and private.craftOpen
end
function ProfessionState.SetCraftOpen(open)
private.craftOpen = open
end
function ProfessionState.GetCurrentProfession()
return private.professionName
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.CreateFSM()
if TSM.IsWowClassic() and not IsAddOnLoaded("Blizzard_CraftUI") then
LoadAddOn("Blizzard_CraftUI")
end
Event.Register("TRADE_SKILL_SHOW", function()
private.tradeSkillOpen = true
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("TRADE_SKILL_CLOSE", function()
private.tradeSkillOpen = false
if not private.craftOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end
end)
if not TSM.IsWowClassic() then
Event.Register("GARRISON_TRADESKILL_NPC_CLOSED", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end)
Event.Register("TRADE_SKILL_DATA_SOURCE_CHANGED", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("TRADE_SKILL_DATA_SOURCE_CHANGING", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
end)
else
Event.Register("CRAFT_SHOW", function()
private.craftOpen = true
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("CRAFT_CLOSE", function()
private.craftOpen = false
if not private.tradeSkillOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end
end)
Event.Register("CRAFT_UPDATE", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
end
local function ToggleDefaultCraftButton()
if not CraftCreateButton then
return
end
if private.craftOpen then
CraftCreateButton:Show()
else
CraftCreateButton:Hide()
end
end
local function FrameDelayCallback()
private.fsm:ProcessEvent("EV_FRAME_DELAY")
end
private.fsm = FSM.New("PROFESSION_STATE")
:AddState(FSM.NewState("ST_CLOSED")
:SetOnEnter(function()
private.isClosed = true
private.RunUpdateCallbacks()
end)
:SetOnExit(function()
private.isClosed = false
private.RunUpdateCallbacks()
end)
:AddTransition("ST_WAITING_FOR_DATA")
:AddEventTransition("EV_TRADE_SKILL_SHOW", "ST_WAITING_FOR_DATA")
)
:AddState(FSM.NewState("ST_WAITING_FOR_DATA")
:AddTransition("ST_WAITING_FOR_READY")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGED", "ST_WAITING_FOR_READY")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_WAITING_FOR_READY")
:SetOnEnter(function()
Delay.AfterFrame("PROFESSION_STATE_TIME", WAIT_FRAME_DELAY, FrameDelayCallback, WAIT_FRAME_DELAY)
end)
:SetOnExit(function()
Delay.Cancel("PROFESSION_STATE_TIME")
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_DATA_CHANGING")
:AddTransition("ST_CLOSED")
:AddEvent("EV_FRAME_DELAY", function()
if TSM.Crafting.ProfessionUtil.IsDataStable() then
return "ST_SHOWN"
end
end)
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGING", "ST_DATA_CHANGING")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function()
local name = TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()
assert(name)
Log.Info("Showing profession: %s", name)
private.professionName = name
if TSM.IsWowClassic() then
ToggleDefaultCraftButton()
end
private.RunUpdateCallbacks()
end)
:SetOnExit(function()
private.professionName = nil
private.RunUpdateCallbacks()
end)
:AddTransition("ST_DATA_CHANGING")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGING", "ST_DATA_CHANGING")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_DATA_CHANGING")
:AddTransition("ST_WAITING_FOR_READY")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGED", "ST_WAITING_FOR_READY")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:Init("ST_CLOSED")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RunUpdateCallbacks()
for _, callback in ipairs(private.updateCallbacks) do
callback(private.professionName)
end
end

View File

@@ -0,0 +1,480 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionUtil = TSM.Crafting:NewPackage("ProfessionUtil")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Delay = TSM.Include("Util.Delay")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
craftQuantity = nil,
craftSpellId = nil,
craftCallback = nil,
craftName = nil,
castingTimeout = nil,
craftTimeout = nil,
preparedSpellId = nil,
preparedTime = 0,
categoryInfoTemp = {},
}
local PROFESSION_LOOKUP = {
["Costura"] = "Sastrería",
["Marroquinería"] = "Peletería",
["Ingénierie"] = "Ingénieur",
["Secourisme"] = "Premiers soins",
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionUtil.OnInitialize()
Event.Register("UNIT_SPELLCAST_SUCCEEDED", function(_, unit, _, spellId)
if unit ~= "player" then
return
end
if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
return
end
-- check if we need to update bank quantity manually
for _, itemString, quantity in TSM.Crafting.MatIterator(private.craftSpellId) do
local bankUsed = quantity - (Inventory.GetBagQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString))
if bankUsed > 0 and bankUsed <= Inventory.GetBankQuantity(itemString) then
Log.Info("Used %d from bank", bankUsed)
BagTracking.ForceBankQuantityDeduction(itemString, bankUsed)
end
end
local callback = private.craftCallback
assert(callback)
private.craftQuantity = private.craftQuantity - 1
private.DoCraftCallback(true, private.craftQuantity == 0)
-- ignore profession updates from crafting something
TSM.Crafting.ProfessionScanner.IgnoreNextProfessionUpdates()
-- restart the timeout
end)
local function SpellcastFailedEventHandler(_, unit, _, spellId)
if unit ~= "player" then
return
end
if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
return
end
private.DoCraftCallback(false, true)
end
local function ClearCraftCast()
private.craftQuantity = nil
private.craftSpellId = nil
private.craftName = nil
private.castingTimeout = nil
private.craftTimeout = nil
end
Event.Register("UNIT_SPELLCAST_INTERRUPTED", SpellcastFailedEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED", SpellcastFailedEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED_QUIET", SpellcastFailedEventHandler)
Event.Register("TRADE_SKILL_CLOSE", ClearCraftCast)
if TSM.IsWowClassic() then
Event.Register("CRAFT_CLOSE", ClearCraftCast)
end
end
function ProfessionUtil.GetCurrentProfessionName()
if TSM.IsWowClassic() then
local name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftSkillLine(1) or GetTradeSkillLine()
return name
else
local _, name, _, _, _, _, parentName = C_TradeSkillUI.GetTradeSkillLine()
return parentName or name
end
end
function ProfessionUtil.GetResultInfo(spellId)
-- get the links
local itemLink = ProfessionUtil.GetRecipeInfo(spellId)
assert(itemLink, "Invalid craft: "..tostring(spellId))
if strfind(itemLink, "enchant:") then
-- result of craft is not an item
local itemString = ProfessionInfo.GetIndirectCraftResult(spellId)
if itemString and not TSM.IsWowClassic() then
return TSM.UI.GetColoredItemName(itemString), itemString, ItemInfo.GetTexture(itemString)
elseif ProfessionInfo.IsEngineeringTinker(spellId) then
local name, _, icon = GetSpellInfo(spellId)
return name, nil, icon
else
local name, _, icon = GetSpellInfo(TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftInfo(TSM.IsWowClassic() and TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId) or spellId)
return name, nil, icon
end
elseif strfind(itemLink, "item:") then
-- result of craft is an item
return TSM.UI.GetColoredItemName(itemLink), ItemString.Get(itemLink), ItemInfo.GetTexture(itemLink)
else
error("Invalid craft: "..tostring(spellId))
end
end
function ProfessionUtil.GetNumCraftable(spellId)
local num, numAll = math.huge, math.huge
for i = 1, ProfessionUtil.GetNumMats(spellId) do
local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
local itemString = ItemString.Get(matItemLink)
local totalQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
if not itemString or not quantity or totalQuantity == 0 then
return 0, 0
end
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
num = min(num, floor(bagQuantity / quantity))
numAll = min(numAll, floor(totalQuantity / quantity))
end
if num == math.huge or numAll == math.huge then
return 0, 0
end
return num, numAll
end
function ProfessionUtil.IsCraftable(spellId)
for i = 1, ProfessionUtil.GetNumMats(spellId) do
local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
local itemString = ItemString.Get(matItemLink)
if not itemString or not quantity then
return false
end
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
if floor(bagQuantity / quantity) == 0 then
return false
end
end
return true
end
function ProfessionUtil.GetNumCraftableFromDB(spellId)
local num = math.huge
for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
num = min(num, floor(bagQuantity / quantity))
end
if num == math.huge then
return 0
end
return num
end
function ProfessionUtil.IsEnchant(spellId)
local name = ProfessionUtil.GetCurrentProfessionName()
if name ~= GetSpellInfo(7411) or TSM.IsWowClassic() then
return false
end
if not strfind(C_TradeSkillUI.GetRecipeItemLink(spellId), "enchant:") then
return false
end
local recipeInfo = nil
if not TSM.IsShadowlands() then
recipeInfo = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, recipeInfo) == recipeInfo)
else
recipeInfo = C_TradeSkillUI.GetRecipeInfo(spellId)
end
local altVerb = recipeInfo.alternateVerb
if not TSM.IsShadowlands() then
TempTable.Release(recipeInfo)
end
return altVerb and true or false
end
function ProfessionUtil.OpenProfession(profession, skillId)
if TSM.IsWowClassic() then
if profession == ProfessionInfo.GetName("Mining") then
-- mining needs to be opened as smelting
profession = ProfessionInfo.GetName("Smelting")
end
if PROFESSION_LOOKUP[profession] then
profession = PROFESSION_LOOKUP[profession]
end
CastSpellByName(profession)
else
C_TradeSkillUI.OpenTradeSkill(skillId)
end
end
function ProfessionUtil.PrepareToCraft(spellId, quantity)
quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
if quantity == 0 then
return
end
if ProfessionUtil.IsEnchant(spellId) then
quantity = 1
end
if not TSM.IsWowClassic() then
C_TradeSkillUI.SetRecipeRepeatCount(spellId, quantity)
end
private.preparedSpellId = spellId
private.preparedTime = GetTime()
end
function ProfessionUtil.Craft(spellId, quantity, useVellum, callback)
assert(TSM.Crafting.ProfessionScanner.HasSpellId(spellId))
if private.craftSpellId then
private.craftCallback = callback
private.DoCraftCallback(false, true)
return 0
end
quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
if quantity == 0 then
return 0
end
local isEnchant = ProfessionUtil.IsEnchant(spellId)
if isEnchant then
quantity = 1
elseif spellId ~= private.preparedSpellId or private.preparedTime == GetTime() then
-- We can only craft one of this item due to a bug on Blizzard's end
quantity = 1
end
private.craftQuantity = quantity
private.craftSpellId = spellId
private.craftCallback = callback
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
private.craftName = GetCraftInfo(spellId)
else
private.craftName = GetTradeSkillInfo(spellId)
DoTradeSkill(spellId, quantity)
end
else
C_TradeSkillUI.CraftRecipe(spellId, quantity)
end
if useVellum and isEnchant then
UseItemByName(ItemInfo.GetName(ProfessionInfo.GetVellumItemString()))
end
private.castingTimeout = nil
private.craftTimeout = nil
Delay.AfterTime("PROFESSION_CRAFT_TIMEOUT_MONITOR", 0.5, private.CraftTimeoutMonitor, 0.5)
return quantity
end
function ProfessionUtil.IsDataStable()
return TSM.IsWowClassic() or (C_TradeSkillUI.IsTradeSkillReady() and not C_TradeSkillUI.IsDataSourceChanging())
end
function ProfessionUtil.HasCooldown(spellId)
if TSM.IsWowClassic() then
return GetTradeSkillCooldown(spellId) and true or false
else
return select(2, C_TradeSkillUI.GetRecipeCooldown(spellId)) and true or false
end
end
function ProfessionUtil.GetRemainingCooldown(spellId)
if TSM.IsWowClassic() then
return GetTradeSkillCooldown(spellId)
else
return C_TradeSkillUI.GetRecipeCooldown(spellId)
end
end
function ProfessionUtil.GetRecipeInfo(spellId)
local itemLink, lNum, hNum, toolsStr, hasTools = nil, nil, nil, nil, nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftItemLink(spellId) or GetTradeSkillItemLink(spellId)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
lNum, hNum = 1, 1
toolsStr, hasTools = GetCraftSpellFocus(spellId)
else
lNum, hNum = GetTradeSkillNumMade(spellId)
toolsStr, hasTools = GetTradeSkillTools(spellId)
end
else
itemLink = C_TradeSkillUI.GetRecipeItemLink(spellId)
lNum, hNum = C_TradeSkillUI.GetRecipeNumItemsProduced(spellId)
toolsStr, hasTools = C_TradeSkillUI.GetRecipeTools(spellId)
end
return itemLink, lNum, hNum, toolsStr, hasTools
end
function ProfessionUtil.GetNumMats(spellId)
local numMats = nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
numMats = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftNumReagents(spellId) or GetTradeSkillNumReagents(spellId)
else
numMats = C_TradeSkillUI.GetRecipeNumReagents(spellId)
end
return numMats
end
function ProfessionUtil.GetMatInfo(spellId, index)
local itemLink, name, texture, quantity = nil, nil, nil, nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftReagentItemLink(spellId, index) or GetTradeSkillReagentItemLink(spellId, index)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
name, texture, quantity = GetCraftReagentInfo(spellId, index)
else
name, texture, quantity = GetTradeSkillReagentInfo(spellId, index)
end
else
itemLink = C_TradeSkillUI.GetRecipeReagentItemLink(spellId, index)
name, texture, quantity = C_TradeSkillUI.GetRecipeReagentInfo(spellId, index)
if itemLink then
name = name or ItemInfo.GetName(itemLink)
texture = texture or ItemInfo.GetTexture(itemLink)
end
end
return itemLink, name, texture, quantity
end
function ProfessionUtil.CloseTradeSkill(closeBoth)
if TSM.IsWowClassic() then
if closeBoth then
CloseCraft()
CloseTradeSkill()
else
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
CloseCraft()
else
CloseTradeSkill()
end
end
else
C_TradeSkillUI.CloseTradeSkill()
C_Garrison.CloseGarrisonTradeskillNPC()
end
end
function ProfessionUtil.IsNPCProfession()
return not TSM.IsWowClassic() and C_TradeSkillUI.IsNPCCrafting()
end
function ProfessionUtil.IsLinkedProfession()
if TSM.IsWowClassic() then
return nil, nil
else
return C_TradeSkillUI.IsTradeSkillLinked()
end
end
function ProfessionUtil.IsGuildProfession()
return not TSM.IsWowClassic() and C_TradeSkillUI.IsTradeSkillGuild()
end
function ProfessionUtil.GetCategoryInfo(categoryId)
local name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel = nil, nil, nil, nil, nil
if TSM.IsWowClassic() then
name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftDisplaySkillLine() or (categoryId and GetTradeSkillInfo(categoryId) or nil)
numIndents = 0
parentCategoryId = nil
else
C_TradeSkillUI.GetCategoryInfo(categoryId, private.categoryInfoTemp)
assert(private.categoryInfoTemp.numIndents)
name = private.categoryInfoTemp.name
numIndents = private.categoryInfoTemp.numIndents
parentCategoryId = private.categoryInfoTemp.numIndents ~= 0 and private.categoryInfoTemp.parentCategoryID or nil
currentSkillLevel = private.categoryInfoTemp.skillLineCurrentLevel
maxSkillLevel = private.categoryInfoTemp.skillLineMaxLevel
wipe(private.categoryInfoTemp)
end
return name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel
end
function ProfessionUtil.StoreOptionalMatText(matList, text)
TSM.db.global.internalData.optionalMatTextLookup[matList] = TSM.db.global.internalData.optionalMatTextLookup[matList] or text
end
function ProfessionUtil.GetOptionalMatText(matList)
return TSM.db.global.internalData.optionalMatTextLookup[matList]
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.DoCraftCallback(result, isDone)
local callback = private.craftCallback
assert(callback)
-- reset timeouts
private.castingTimeout = nil
private.craftTimeout = nil
if isDone then
private.craftQuantity = nil
private.craftSpellId = nil
private.craftCallback = nil
private.craftName = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
end
callback(result, isDone)
end
function private.CraftTimeoutMonitor()
if not private.craftSpellId then
Log.Info("No longer crafting")
private.castingTimeout = nil
private.craftTimeout = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
return
end
local _, _, _, _, castEndTimeMs, _, _, _, spellId = private.GetPlayerCastingInfo()
if spellId then
private.castingTimeout = nil
else
private.craftTimeout = nil
end
if not spellId then
-- no active cast
if GetTime() > (private.castingTimeout or math.huge) then
Log.Err("Craft timed out (%s)", private.craftSpellId)
private.DoCraftCallback(false, true)
return
end
-- set the casting timeout to 1 second from now
private.castingTimeout = GetTime() + 1
return
elseif private.craftSpellId ~= spellId then
Log.Err("Crafting something else (%s, %s)", private.craftSpellId, spellId)
private.castingTimeout = nil
private.craftTimeout = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
return
end
if GetTime() > (private.craftTimeout or math.huge) then
Log.Err("Craft timed out (%s)", private.craftSpellId)
private.DoCraftCallback(false, true)
return
end
-- set the timeout to 1 second after the end time
private.craftTimeout = castEndTimeMs / 1000 + 1
end
function private.GetPlayerCastingInfo()
if TSM.IsWowClassic() then
return CastingInfo()
else
return UnitCastingInfo("player")
end
end

View File

@@ -0,0 +1,186 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Queue = TSM.Crafting:NewPackage("Queue")
local Database = TSM.Include("Util.Database")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Inventory = TSM.Include("Service.Inventory")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
db = nil,
}
local MAX_NUM_QUEUED = 9999
-- ============================================================================
-- Module Functions
-- ============================================================================
function Queue.OnEnable()
private.db = Database.NewSchema("CRAFTING_QUEUE")
:AddUniqueNumberField("spellId")
:AddNumberField("num")
:Commit()
private.db:SetQueryUpdatesPaused(true)
for spellId, data in pairs(TSM.db.factionrealm.internalData.crafts) do
Queue.SetNum(spellId, data.queued) -- sanitize / cache the number queued
end
private.db:SetQueryUpdatesPaused(false)
end
function Queue.GetDBForJoin()
return private.db
end
function Queue.CreateQuery()
return private.db:NewQuery()
end
function Queue.SetNum(spellId, num)
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
if not craftInfo then
Log.Err("Could not find craft: "..spellId)
return
end
craftInfo.queued = min(max(Math.Round(num or 0), 0), MAX_NUM_QUEUED)
local query = private.db:NewQuery()
:Equal("spellId", spellId)
local row = query:GetFirstResult()
if row and craftInfo.queued == 0 then
-- delete this row
private.db:DeleteRow(row)
elseif row then
-- update this row
row:SetField("num", craftInfo.queued)
:Update()
elseif craftInfo.queued > 0 then
-- insert a new row
private.db:NewRow()
:SetField("spellId", spellId)
:SetField("num", craftInfo.queued)
:Create()
end
query:Release()
end
function Queue.GetNum(spellId)
return private.db:GetUniqueRowField("spellId", spellId, "num") or 0
end
function Queue.Add(spellId, quantity)
Queue.SetNum(spellId, Queue.GetNum(spellId) + quantity)
end
function Queue.Remove(spellId, quantity)
Queue.SetNum(spellId, Queue.GetNum(spellId) - quantity)
end
function Queue.Clear()
local query = private.db:NewQuery()
:Select("spellId")
for _, spellId in query:Iterator() do
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
if craftInfo then
craftInfo.queued = 0
end
end
query:Release()
private.db:Truncate()
end
function Queue.GetNumItems()
return private.db:NewQuery():CountAndRelease()
end
function Queue.GetTotals()
local totalCost, totalProfit, totalCastTimeMs, totalNumQueued = nil, nil, nil, 0
local query = private.db:NewQuery()
:Select("spellId", "num")
for _, spellId, numQueued in query:Iterator() do
local numResult = TSM.db.factionrealm.internalData.crafts[spellId] and TSM.db.factionrealm.internalData.crafts[spellId].numResult or 0
local cost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(spellId)
if cost then
totalCost = (totalCost or 0) + cost * numQueued * numResult
end
if profit then
totalProfit = (totalProfit or 0) + profit * numQueued * numResult
end
local castTime = select(4, GetSpellInfo(spellId))
if castTime then
totalCastTimeMs = (totalCastTimeMs or 0) + castTime * numQueued
end
totalNumQueued = totalNumQueued + numQueued
end
query:Release()
return totalCost, totalProfit, totalCastTimeMs and ceil(totalCastTimeMs / 1000) or nil, totalNumQueued
end
function Queue.RestockGroups(groups)
private.db:SetQueryUpdatesPaused(true)
for _, groupPath in ipairs(groups) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
if TSM.Crafting.CanCraftItem(itemString) then
local isValid, err = TSM.Operations.Crafting.IsValid(itemString)
if isValid then
private.RestockItem(itemString)
elseif err then
Log.PrintUser(err)
end
end
end
end
end
private.db:SetQueryUpdatesPaused(false)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RestockItem(itemString)
local cheapestCost, cheapestSpellId = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
if not cheapestSpellId then
-- can't craft this item
return
end
local itemValue = TSM.Crafting.Cost.GetCraftedItemValue(itemString)
local profit = itemValue and cheapestCost and (itemValue - cheapestCost) or nil
local hasMinProfit, minProfit = TSM.Operations.Crafting.GetMinProfit(itemString)
if hasMinProfit and (not minProfit or not profit or profit < minProfit) then
-- profit is too low
return
end
local haveQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
for guild, ignored in pairs(TSM.db.global.craftingOptions.ignoreGuilds) do
if ignored then
haveQuantity = haveQuantity - Inventory.GetGuildQuantity(itemString, guild)
end
end
for player, ignored in pairs(TSM.db.global.craftingOptions.ignoreCharacters) do
if ignored then
haveQuantity = haveQuantity - Inventory.GetBagQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetBankQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetReagentBankQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetAuctionQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetMailQuantity(itemString, player)
end
end
assert(haveQuantity >= 0)
local neededQuantity = TSM.Operations.Crafting.GetRestockQuantity(itemString, haveQuantity)
if neededQuantity == 0 then
return
end
-- queue only if it satisfies all operation criteria
Queue.SetNum(cheapestSpellId, floor(neededQuantity / TSM.Crafting.GetNumResult(cheapestSpellId)))
end

View File

@@ -0,0 +1,227 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CraftingSync = TSM.Crafting:NewPackage("Sync")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Theme = TSM.Include("Util.Theme")
local Sync = TSM.Include("Service.Sync")
local private = {
hashesTemp = {},
spellsTemp = {},
spellsProfessionLookupTemp = {},
spellInfoTemp = {
spellIds = {},
mats = {},
itemStrings = {},
names = {},
numResults = {},
hasCDs = {},
},
accountLookup = {},
accountStatus = {},
}
local RETRY_DELAY = 5
local PROFESSION_HASH_FIELDS = { "spellId", "itemString" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function CraftingSync.OnInitialize()
Sync.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
Sync.RegisterRPC("CRAFTING_GET_HASHES", private.RPCGetHashes)
Sync.RegisterRPC("CRAFTING_GET_SPELLS", private.RPCGetSpells)
Sync.RegisterRPC("CRAFTING_GET_SPELL_INFO", private.RPCGetSpellInfo)
end
function CraftingSync.GetStatus(account)
local status = private.accountStatus[account]
if not status then
return Theme.GetFeedbackColor("RED"):ColorText(L["Not Connected"])
elseif status == "UPDATING" or status == "RETRY" then
return Theme.GetFeedbackColor("YELLOW"):ColorText(L["Updating"])
elseif status == "SYNCED" then
return Theme.GetFeedbackColor("GREEN"):ColorText(L["Up to date"])
else
error("Invalid status: "..tostring(status))
end
end
-- ============================================================================
-- RPC Functions and Result Handlers
-- ============================================================================
function private.RPCGetHashes()
wipe(private.hashesTemp)
local player = UnitName("player")
private.GetPlayerProfessionHashes(player, private.hashesTemp)
return player, private.hashesTemp
end
function private.RPCGetHashesResultHandler(player, data)
if not player or not private.accountLookup[player] then
-- request timed out, so try again
Log.Warn("Getting hashes timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
local currentInfo = TempTable.Acquire()
private.GetPlayerProfessionHashes(player, currentInfo)
local requestProfessions = TempTable.Acquire()
for profession, hash in pairs(data) do
if hash == currentInfo[profession] then
Log.Info("%s data for %s already up to date", profession, player)
else
Log.Info("Need updated %s data from %s (%s, %s)", profession, player, hash, tostring(currentInfo[hash]))
requestProfessions[profession] = true
end
end
TempTable.Release(currentInfo)
if next(requestProfessions) then
private.accountStatus[private.accountLookup[player]] = "UPDATING"
Sync.CallRPC("CRAFTING_GET_SPELLS", player, private.RPCGetSpellsResultHandler, requestProfessions)
else
private.accountStatus[private.accountLookup[player]] = "SYNCED"
end
TempTable.Release(requestProfessions)
end
function private.RPCGetSpells(professions)
wipe(private.spellsProfessionLookupTemp)
wipe(private.spellsTemp)
local player = UnitName("player")
local query = TSM.Crafting.CreateRawCraftsQuery()
:Select("spellId", "profession")
:Custom(private.QueryProfessionFilter, professions)
:Custom(private.QueryPlayerFilter, player)
:OrderBy("spellId", true)
for _, spellId, profession in query:Iterator() do
private.spellsProfessionLookupTemp[spellId] = profession
tinsert(private.spellsTemp, spellId)
end
query:Release()
return player, private.spellsProfessionLookupTemp, private.spellsTemp
end
function private.RPCGetSpellsResultHandler(player, professionLookup, spells)
if not player or not private.accountLookup[player] then
-- request timed out, so try again from the start
Log.Warn("Getting spells timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
for i = #spells, 1, -1 do
local spellId = spells[i]
if TSM.Crafting.HasSpellId(spellId) then
-- already have this spell so just make sure this player is added
TSM.Crafting.AddPlayer(spellId, player)
tremove(spells, i)
end
end
if #spells == 0 then
Log.Info("Spells up to date for %s", player)
private.accountStatus[private.accountLookup[player]] = "SYNCED"
else
Log.Info("Requesting %d spells from %s", #spells, player)
Sync.CallRPC("CRAFTING_GET_SPELL_INFO", player, private.RPCGetSpellInfoResultHandler, professionLookup, spells)
end
end
function private.RPCGetSpellInfo(professionLookup, spells)
for _, tbl in pairs(private.spellInfoTemp) do
wipe(tbl)
end
for i, spellId in ipairs(spells) do
private.spellInfoTemp.spellIds[i] = spellId
private.spellInfoTemp.mats[i] = TSM.db.factionrealm.internalData.crafts[spellId].mats
private.spellInfoTemp.itemStrings[i] = TSM.db.factionrealm.internalData.crafts[spellId].itemString
private.spellInfoTemp.names[i] = TSM.db.factionrealm.internalData.crafts[spellId].name
private.spellInfoTemp.numResults[i] = TSM.db.factionrealm.internalData.crafts[spellId].numResult
private.spellInfoTemp.hasCDs[i] = TSM.db.factionrealm.internalData.crafts[spellId].hasCD
end
Log.Info("Sent %d spells", #private.spellInfoTemp.spellIds)
return UnitName("player"), professionLookup, private.spellInfoTemp
end
function private.RPCGetSpellInfoResultHandler(player, professionLookup, spellInfo)
if not player or not professionLookup or not spellInfo or not private.accountLookup[player] then
-- request timed out, so try again from the start
Log.Warn("Getting spell info timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
for i, spellId in ipairs(spellInfo.spellIds) do
TSM.Crafting.CreateOrUpdate(spellId, spellInfo.itemStrings[i], professionLookup[spellId], spellInfo.names[i], spellInfo.numResults[i], player, spellInfo.hasCDs[i] and true or false)
for itemString in pairs(spellInfo.mats[i]) do
TSM.db.factionrealm.internalData.mats[itemString] = TSM.db.factionrealm.internalData.mats[itemString] or {}
end
TSM.Crafting.SetMats(spellId, spellInfo.mats[i])
end
Log.Info("Added %d spells from %s", #spellInfo.spellIds, player)
private.accountStatus[private.accountLookup[player]] = "SYNCED"
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ConnectionChangedHandler(account, player, connected)
if connected then
private.accountLookup[player] = account
private.accountStatus[account] = "UPDATING"
-- issue a request for profession info
Sync.CallRPC("CRAFTING_GET_HASHES", player, private.RPCGetHashesResultHandler)
else
private.accountLookup[player] = nil
private.accountStatus[account] = nil
end
end
function private.RetryGetHashesRPC()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "RETRY" then
Sync.CallRPC("CRAFTING_GET_HASHES", player, private.RPCGetHashesResultHandler)
end
end
end
function private.QueryProfessionFilter(row, professions)
return professions[row:GetField("profession")]
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.GetPlayerProfessionHashes(player, resultTbl)
local query = TSM.Crafting.CreateRawCraftsQuery()
:Custom(private.QueryPlayerFilter, player)
:OrderBy("spellId", true)
query:GroupedHash(PROFESSION_HASH_FIELDS, "profession", resultTbl)
query:Release()
end