initial commit
This commit is contained in:
776
Core/Service/Crafting/Core.lua
Normal file
776
Core/Service/Crafting/Core.lua
Normal 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
|
||||
156
Core/Service/Crafting/Cost.lua
Normal file
156
Core/Service/Crafting/Cost.lua
Normal 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
|
||||
525
Core/Service/Crafting/Gathering.lua
Normal file
525
Core/Service/Crafting/Gathering.lua
Normal 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
|
||||
303
Core/Service/Crafting/PlayerProfessions.lua
Normal file
303
Core/Service/Crafting/PlayerProfessions.lua
Normal 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
|
||||
554
Core/Service/Crafting/ProfessionScanner.lua
Normal file
554
Core/Service/Crafting/ProfessionScanner.lua
Normal 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
|
||||
191
Core/Service/Crafting/ProfessionState.lua
Normal file
191
Core/Service/Crafting/ProfessionState.lua
Normal 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
|
||||
480
Core/Service/Crafting/ProfessionUtil.lua
Normal file
480
Core/Service/Crafting/ProfessionUtil.lua
Normal 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
|
||||
186
Core/Service/Crafting/Queue.lua
Normal file
186
Core/Service/Crafting/Queue.lua
Normal 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
|
||||
227
Core/Service/Crafting/Sync.lua
Normal file
227
Core/Service/Crafting/Sync.lua
Normal 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
|
||||
Reference in New Issue
Block a user