-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local TSM = select ( 2 , ... ) ---@type TSM
local Scanner = TSM.Init ( " Service.ProfessionHelpers.Scanner " ) ---@class Service.ProfessionHelpers.Scanner
local Environment = TSM.Include ( " Environment " )
local State = TSM.Include ( " Service.ProfessionHelpers.State " )
local Quality = TSM.Include ( " Service.ProfessionHelpers.Quality " )
local ProfessionInfo = TSM.Include ( " Data.ProfessionInfo " )
local CraftString = TSM.Include ( " Util.CraftString " )
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 Table = TSM.Include ( " Util.Table " )
local Math = TSM.Include ( " Util.Math " )
local Log = TSM.Include ( " Util.Log " )
local ItemString = TSM.Include ( " Util.ItemString " )
local MatString = TSM.Include ( " Util.MatString " )
local ItemInfo = TSM.Include ( " Service.ItemInfo " )
local private = {
db = nil ,
matDB = nil ,
dbPopulated = false ,
hasScanned = false ,
callbacks = { } ,
disabled = false ,
ignoreUpdatesUntil = 0 ,
recipeInfoCache = { } ,
prevScannedHash = nil ,
scanTimer = nil ,
resultQualityTemp = { } ,
classicSpellIdLookup = { } ,
scanHookFunc = nil ,
inactiveFunc = nil ,
categorySkillLevelCache = {
lastUpdate = 0 ,
} ,
matStringItemsTemp = { } ,
matQuantitiesTemp = { } ,
matIteratorQuery = 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
local MAX_CRAFT_LEVEL = 4
local EMPTY_MATS_TABLE = { }
local MAT_STRING_OPTIONAL_MATCH_STR = {
[ MatString.TYPE . OPTIONAL ] = " ^o: " ,
[ MatString.TYPE . FINISHING ] = " ^f: " ,
}
local SCAN_HASH_INFO_FIELDS = {
" index " ,
" previousRecipeID " ,
" nextRecipeID " ,
" categoryID " ,
" learned " ,
" unlockedRecipeLevel " ,
" relativeDifficulty " ,
" numSkillUps " ,
" name " ,
" currentRecipeExperience " ,
" nextLevelRecipeExperience " ,
" qualityIlvlBonuses " ,
}
local INSPIRATION_DESC_PATTERN = nil
do
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
INSPIRATION_DESC_PATTERN = gsub ( PROFESSIONS_CRAFTING_STAT_TT_CRIT_DESC , " %%d " , " ([0-9%.]+) " )
if INSPIRATION_DESC_PATTERN == PROFESSIONS_CRAFTING_STAT_TT_CRIT_DESC then
-- This locale uses positional format specifiers, so we'll use a different mechanism to try to extract the inspiration
INSPIRATION_DESC_PATTERN = nil
end
end
end
-- ============================================================================
-- Module Functions
-- ============================================================================
Scanner : OnModuleLoad ( function ( )
local dbSchema = Database.NewSchema ( " CRAFTING_RECIPES " )
: AddUniqueStringField ( " craftString " )
: AddStringField ( " itemString " )
: AddNumberField ( " index " )
: AddStringField ( " name " )
: AddStringField ( " craftName " )
: AddNumberField ( " categoryId " )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
dbSchema : AddNumberField ( " difficulty " )
else
dbSchema : AddStringField ( " difficulty " )
end
private.db = dbSchema
: AddNumberField ( " rank " )
: AddNumberField ( " numSkillUps " )
: AddNumberField ( " level " )
: AddNumberField ( " currentExp " )
: AddNumberField ( " nextExp " )
: Commit ( )
private.matDB = Database.NewSchema ( " CRAFTING_RECIPE_MATS " )
: AddStringField ( " craftString " )
: AddStringField ( " matString " )
: AddNumberField ( " quantity " )
: AddStringField ( " slotText " )
: AddIndex ( " craftString " )
: AddIndex ( " matString " )
: Commit ( )
private.matIteratorQuery = private.matDB : NewQuery ( )
: Select ( " matString " , " quantity " , " slotText " )
: Equal ( " craftString " , Database.BoundQueryParam ( ) )
private.scanTimer = Delay.CreateTimer ( " PROFESSION_SCAN " , private.ScanProfession )
State.RegisterCallback ( private.ProfessionStateUpdate )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
Event.Register ( " TRADE_SKILL_LIST_UPDATE " , private.OnTradeSkillUpdateEvent )
else
Event.Register ( " CRAFT_UPDATE " , private.OnTradeSkillUpdateEvent )
Event.Register ( " TRADE_SKILL_UPDATE " , private.OnTradeSkillUpdateEvent )
end
Event.Register ( " CHAT_MSG_SKILL " , private.ChatMsgSkillEventHandler )
end )
function Scanner . SetDisabled ( disabled )
if private.disabled == disabled then
return
end
private.disabled = disabled
if not disabled then
private.ScanProfession ( )
end
end
function Scanner . HasScanned ( )
return private.hasScanned
end
function Scanner . HasSkills ( )
return private.hasScanned and private.db : GetNumRows ( ) > 0
end
function Scanner . RegisterHasScannedCallback ( callback )
tinsert ( private.callbacks , callback )
end
function Scanner . IgnoreNextProfessionUpdates ( )
private.ignoreUpdatesUntil = GetTime ( ) + 1
end
function Scanner . CreateQuery ( )
return private.db : NewQuery ( )
end
function Scanner . GetItemStringByCraftString ( craftString )
assert ( private.dbPopulated )
local itemString = private.db : GetUniqueRowField ( " craftString " , craftString , " itemString " )
return itemString ~= " " and itemString or nil
end
function Scanner . GetIndexByCraftString ( craftString )
assert ( not Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) or private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " index " )
end
function Scanner . GetCategoryIdByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " categoryId " )
end
function Scanner . GetNameByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " name " )
end
function Scanner . GetCraftNameByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " craftName " )
end
function Scanner . GetCurrentExpByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " currentExp " )
end
function Scanner . GetNextExpByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " nextExp " )
end
function Scanner . GetDifficultyByCraftString ( craftString )
assert ( private.dbPopulated )
return private.db : GetUniqueRowField ( " craftString " , craftString , " difficulty " )
end
function Scanner . HasCraftString ( craftString )
return private.dbPopulated and private.db : HasUniqueRow ( " craftString " , craftString )
end
function Scanner . MatIterator ( craftString )
return private.matIteratorQuery : BindParams ( craftString )
: Iterator ( )
end
function Scanner . GetOptionalMatString ( craftString , slotId )
return private.matDB : NewQuery ( )
: Select ( " matString " )
: Equal ( " craftString " , craftString )
: Matches ( " matString " , " ^[qofr]: " )
: Contains ( " matString " , " : " .. slotId .. " : " )
: GetSingleResult ( )
end
function Scanner . GetNumOptionalMats ( craftString , matType )
local matchStr = MAT_STRING_OPTIONAL_MATCH_STR [ matType ]
assert ( matchStr )
return private.matDB : NewQuery ( )
: Equal ( " craftString " , craftString )
: Matches ( " matString " , matchStr )
: CountAndRelease ( )
end
function Scanner . GetMatQuantity ( craftString , matItemId )
local query = private.matDB : NewQuery ( )
: Select ( " quantity " )
: Equal ( " craftString " , craftString )
: Matches ( " matString " , " ^[qofr]: " )
: Contains ( " matString " , tostring ( matItemId ) )
return query : GetFirstResultAndRelease ( )
end
function Scanner . GetMatSlotText ( craftString , matString )
return private.matDB : NewQuery ( )
: Select ( " slotText " )
: Equal ( " craftString " , craftString )
: Equal ( " matString " , matString )
: GetSingleResultAndRelease ( )
end
function Scanner . SetHookFuncs ( scanFunc , inactiveFunc )
assert ( inactiveFunc and scanFunc )
assert ( not private.scanHookFunc and not private.inactiveFunc )
private.scanHookFunc = scanFunc
private.inactiveFunc = inactiveFunc
end
function Scanner . GetRecipeQualityInfo ( craftString )
if not Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
return nil , nil , nil , nil , nil
end
local spellId = CraftString.GetSpellId ( craftString )
local info = C_TradeSkillUI.GetRecipeSchematic ( spellId , false , 1 )
local isItem = info.recipeType == Enum.TradeskillRecipeType . Item
local isEnchant = info.recipeType == Enum.TradeskillRecipeType . Enchant
local isSalvage = info.recipeType == Enum.TradeskillRecipeType . Salvage
if not info.hasCraftingOperationInfo or info.hasGatheringOperationInfo then
return nil , nil , nil , nil , nil
elseif not isItem and not isEnchant and not isSalvage then
return nil , nil , nil , nil , nil
elseif isItem and not info.outputItemID then
return nil , nil , nil , nil , nil
end
local operationInfo = C_TradeSkillUI.GetCraftingOperationInfo ( spellId , EMPTY_MATS_TABLE )
if not operationInfo then
return nil , nil , nil , nil , nil
end
local inspirationChance , inspirationAmount = 0 , 0
for _ , statInfo in ipairs ( operationInfo.bonusStats ) do
if statInfo.bonusStatName == PROFESSIONS_CRAFTING_STAT_TT_CRIT_HEADER then
if INSPIRATION_DESC_PATTERN then
inspirationChance , inspirationAmount = strmatch ( statInfo.ratingDescription , INSPIRATION_DESC_PATTERN )
end
if not inspirationChance or inspirationChance == 0 then
-- Try another way to parse the chance / amount
inspirationChance = strmatch ( statInfo.ratingDescription , " ([0-9%.]+)%% " )
inspirationAmount = strmatch ( statInfo.ratingDescription , " ([0-9]+)[^%%%.,] " )
end
inspirationChance = tonumber ( inspirationChance ) / 100
inspirationAmount = tonumber ( inspirationAmount )
assert ( inspirationChance and inspirationAmount )
break
end
end
local hasQualityMats = false
for _ , data in ipairs ( info.reagentSlotSchematics ) do
if data.reagentType == Enum.CraftingReagentType . Basic and data.dataSlotType == Enum.TradeskillSlotDataType . ModifiedReagent then
hasQualityMats = true
break
end
end
return operationInfo.baseDifficulty , operationInfo.quality , hasQualityMats , inspirationAmount , inspirationChance
end
---TODO: Should this handle indirect crafts on classic like GetResultInfo()?
function Scanner . GetResultItem ( craftString )
local spellId = CraftString.GetSpellId ( craftString )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local indirectResult = ProfessionInfo.GetIndirectCraftResult ( spellId )
if indirectResult then
if type ( indirectResult ) == " table " then
for i = 1 , # indirectResult do
local link = ItemInfo.GetLink ( indirectResult [ i ] )
assert ( link )
indirectResult [ i ] = link
end
else
indirectResult = ItemInfo.GetLink ( indirectResult )
assert ( indirectResult )
end
return indirectResult , spellId
end
local result = C_TradeSkillUI.GetRecipeQualityItemIDs ( spellId )
if not result then
result = C_TradeSkillUI.GetRecipeItemLink ( spellId )
local baseItemString = result and ItemString.GetBase ( result ) or nil
if not baseItemString then
return result
end
local ilvlBonuses = C_TradeSkillUI.GetRecipeInfo ( spellId ) . qualityIlvlBonuses
if not ilvlBonuses then
return result
end
local baseLevel = GetDetailedItemLevelInfo ( result )
assert ( baseLevel )
result = ilvlBonuses
wipe ( private.resultQualityTemp )
for i = 1 , # result do
local relLevel = result [ i ]
assert ( relLevel >= 0 )
local itemString = baseItemString .. " ::i " .. ( baseLevel + relLevel )
result [ i ] = baseItemString .. " ::i " .. ( baseLevel + relLevel )
private.resultQualityTemp [ itemString ] = relLevel
end
else
wipe ( private.resultQualityTemp )
for i = 1 , # result do
local itemId = result [ i ]
local itemString = " i: " .. itemId
result [ i ] = itemString
local quality = C_TradeSkillUI.GetItemCraftedQualityByItemInfo ( itemId ) or C_TradeSkillUI.GetItemReagentQualityByItemInfo ( itemId ) or ItemInfo.GetCraftedQuality ( itemString )
assert ( quality )
private.resultQualityTemp [ itemString ] = quality
end
end
Table.SortWithValueLookup ( result , private.resultQualityTemp )
for i = 1 , # result do
local link = ItemInfo.GetLink ( result [ i ] )
assert ( link )
result [ i ] = link
end
return result
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
local itemLink = State.IsClassicCrafting ( ) and GetCraftItemLink ( spellId ) or GetTradeSkillItemLink ( spellId )
local emptyLink = strfind ( itemLink or " " , " item:: " ) and true or false
itemLink = not emptyLink and itemLink or nil
if Environment.IsWrathClassic ( ) then
itemLink = itemLink or GetTradeSkillRecipeLink ( spellId ) or nil
local indirectSpellId = strmatch ( itemLink , " enchant:(%d+) " )
indirectSpellId = indirectSpellId and tonumber ( indirectSpellId )
local itemString = ProfessionInfo.GetIndirectCraftResult ( indirectSpellId )
itemLink = itemString and ItemInfo.GetLink ( itemString ) or itemLink
return itemLink , indirectSpellId
else
return itemLink
end
end
end
function Scanner . GetVellumItemString ( craftString )
if Environment.IsWrathClassic ( ) then
local spellId = CraftString.GetSpellId ( craftString )
spellId = private.classicSpellIdLookup [ spellId ] or spellId
local itemLink = ( State.IsClassicCrafting ( ) and GetCraftItemLink ( spellId ) or GetTradeSkillItemLink ( spellId ) ) or GetTradeSkillRecipeLink ( spellId )
local indirectSpellId = itemLink and strmatch ( itemLink , " enchant:(%d+) " )
indirectSpellId = indirectSpellId and tonumber ( indirectSpellId )
assert ( indirectSpellId )
return ProfessionInfo.GetVellumItemString ( indirectSpellId )
end
return ProfessionInfo.GetVellumItemString ( )
end
function Scanner . GetNumResultItems ( craftString )
if not Environment.IsRetail ( ) then
return 1
elseif not CraftString.GetQuality ( craftString ) then
return 1
end
local spellId = CraftString.GetSpellId ( craftString )
local indirectResult = ProfessionInfo.GetIndirectCraftResult ( spellId )
if indirectResult then
return type ( indirectResult ) == " table " and # indirectResult or 1
end
local result = C_TradeSkillUI.GetRecipeQualityItemIDs ( spellId ) or C_TradeSkillUI.GetRecipeInfo ( spellId ) . qualityIlvlBonuses
return result and # result or 1
end
function Scanner . GetCraftedQuantityRange ( craftString )
if State.IsClassicCrafting ( ) then
return 1 , 1
end
local spellId = CraftString.GetSpellId ( craftString )
if Scanner.IsEnchant ( craftString ) then
return 1 , 1
else
-- workaround for incorrect values returned for Temporal Crystal
if spellId == 169092 then
return 1 , 1
end
-- workaround for incorrect values returned for new mass milling recipes
if ProfessionInfo.IsMassMill ( spellId ) then
if spellId == 210116 then -- Yseralline
return 4 , 4 -- always four
elseif spellId == 209664 then -- Felwort
return 42 , 42 -- amount is variable but the values are conservative
elseif spellId == 247861 then -- Astral Glory
return 4 , 4 -- amount is variable but the values are conservative
else
return 8 , 8.8
end
end
end
local lNum , hNum = nil , nil
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local info = C_TradeSkillUI.GetRecipeSchematic ( spellId , false )
lNum , hNum = info.quantityMin , info.quantityMax
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
lNum , hNum = GetTradeSkillNumMade ( spellId )
end
return lNum , hNum
end
function Scanner . IsEnchant ( craftString )
if State.GetSkillLine ( ) ~= GetSpellInfo ( 7411 ) then
return false
end
local spellId = CraftString.GetSpellId ( craftString )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
if not strfind ( C_TradeSkillUI.GetRecipeItemLink ( spellId ) , " enchant: " ) then
return false
end
return C_TradeSkillUI.GetRecipeInfo ( spellId ) . isEnchantingRecipe
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
local itemLink = State.IsClassicCrafting ( ) and GetCraftItemLink ( spellId ) or GetTradeSkillItemLink ( spellId )
itemLink = itemLink or ( Environment.IsWrathClassic ( ) and GetTradeSkillRecipeLink ( spellId ) ) or nil
if not itemLink or not strfind ( itemLink , " enchant: " ) then
return false
end
return true
end
end
function Scanner . HasCooldown ( craftString )
local spellId = CraftString.GetSpellId ( craftString )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
return select ( 2 , C_TradeSkillUI.GetRecipeCooldown ( spellId ) ) and true or false
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
return GetTradeSkillCooldown ( spellId ) and true or false
end
end
function Scanner . GetNumMats ( craftString )
local spellId = CraftString.GetSpellId ( craftString )
local numMats = nil
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local level = CraftString.GetLevel ( craftString )
local info = C_TradeSkillUI.GetRecipeSchematic ( spellId , false , level )
local num = 0
for _ , data in ipairs ( info.reagentSlotSchematics ) do
if private.IsRegularMat ( data ) then
num = num + 1
end
end
numMats = num
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
numMats = State.IsClassicCrafting ( ) and GetCraftNumReagents ( spellId ) or GetTradeSkillNumReagents ( spellId )
end
return numMats
end
function Scanner . GetMatInfo ( craftString , index )
local spellId = CraftString.GetSpellId ( craftString )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local info = C_TradeSkillUI.GetRecipeSchematic ( spellId , false , CraftString.GetLevel ( craftString ) )
local reagentSlotInfo = nil
local reagentOffset = index - 1
for _ , data in ipairs ( info.reagentSlotSchematics ) do
if private.IsRegularMat ( data ) then
if reagentOffset == 0 then
reagentSlotInfo = data
break
else
reagentOffset = reagentOffset - 1
end
end
end
assert ( reagentSlotInfo )
if reagentSlotInfo.reagentType == Enum.CraftingReagentType . Modifying and reagentSlotInfo.required then
local itemLink = C_TradeSkillUI.GetRecipeQualityReagentItemLink ( spellId , reagentSlotInfo.dataSlotIndex , 1 )
local itemString = ItemString.Get ( itemLink )
local name , quantity = ItemInfo.GetName ( itemLink ) , reagentSlotInfo.quantityRequired
return itemString , quantity , name , true
elseif reagentSlotInfo.reagentType == Enum.CraftingReagentType . Basic and reagentSlotInfo.dataSlotType == Enum.TradeskillSlotDataType . Reagent then
local itemLink = C_TradeSkillUI.GetRecipeFixedReagentItemLink ( spellId , reagentSlotInfo.dataSlotIndex )
local itemString = ItemString.Get ( itemLink )
local name , quantity = ItemInfo.GetName ( itemLink ) , reagentSlotInfo.quantityRequired
return itemString , quantity , name , false
elseif reagentSlotInfo.reagentType == Enum.CraftingReagentType . Basic and reagentSlotInfo.dataSlotType == Enum.TradeskillSlotDataType . ModifiedReagent then
local qualityIndex = 1
local reagentDataInfo = reagentSlotInfo.reagents [ qualityIndex ]
local itemLink = C_TradeSkillUI.GetRecipeQualityReagentItemLink ( spellId , reagentSlotInfo.dataSlotIndex , qualityIndex )
-- NOTE: For some reason, the above API doesn't always work (i.e. with 'Handful of Serevite Bolts')
local itemString = itemLink and ItemString.Get ( itemLink ) or " i: " .. reagentDataInfo.itemID
itemLink = itemLink or ItemInfo.GetLink ( reagentDataInfo.itemID )
local name , quantity = ItemInfo.GetName ( itemLink ) , reagentSlotInfo.quantityRequired
return itemString , quantity , name , true
else
error ( " Invalid mat: %s, %s " , tostring ( craftString ) , tostring ( index ) )
end
else
spellId = private.classicSpellIdLookup [ spellId ] or spellId
local itemLink = State.IsClassicCrafting ( ) and GetCraftReagentItemLink ( spellId , index ) or GetTradeSkillReagentItemLink ( spellId , index )
local itemString = ItemString.Get ( itemLink )
if State.IsClassicCrafting ( ) then
local name , _ , quantity = GetCraftReagentInfo ( spellId , index )
return itemString , quantity , name , false
else
local name , _ , quantity = GetTradeSkillReagentInfo ( spellId , index )
return itemString , quantity , name , false
end
end
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
function private . ProfessionStateUpdate ( )
private.hasScanned = false
private.dbPopulated = false
for _ , callback in ipairs ( private.callbacks ) do
callback ( )
end
if State.GetCurrentProfession ( ) then
private.db : Truncate ( )
private.prevScannedHash = nil
private.OnTradeSkillUpdateEvent ( )
else
private.scanTimer : Cancel ( )
end
end
function private . OnTradeSkillUpdateEvent ( )
private.scanTimer : Cancel ( )
private.QueueProfessionScan ( )
end
function private . ChatMsgSkillEventHandler ( _ , msg )
local professionName = State.GetCurrentProfession ( )
if not professionName or not strmatch ( msg , professionName ) then
return
end
private.ignoreUpdatesUntil = 0
private.QueueProfessionScan ( )
end
-- ============================================================================
-- Profession Scanning
-- ============================================================================
function private . QueueProfessionScan ( )
private.scanTimer : RunForFrames ( SCAN_DEBOUNCE_FRAMES )
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 = State.GetCurrentProfession ( )
if not professionName or not State.IsDataStable ( ) then
-- profession hasn't fully opened yet
private.QueueProfessionScan ( )
return
end
if private.ClearFilters ( ) then
-- An update event will be triggered
return
end
local scannedHash = nil
local haveInvalidRecipes = false
local haveInvalidMats = false
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
wipe ( private.recipeInfoCache )
local prevRecipeIds = TempTable.Acquire ( )
local nextRecipeIds = TempTable.Acquire ( )
local recipes = TempTable.Acquire ( )
assert ( C_TradeSkillUI.GetFilteredRecipeIDs ( recipes ) == recipes )
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 private.recipeInfoCache [ spellId ] then
local info = C_TradeSkillUI.GetRecipeInfo ( spellId )
assert ( not info.index )
info.index = index
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
private.recipeInfoCache [ spellId ] = info
scannedHash = Math.CalculateHash ( spellId , scannedHash )
for _ , hashField in ipairs ( SCAN_HASH_INFO_FIELDS ) do
scannedHash = Math.CalculateHash ( info [ hashField ] , scannedHash )
end
end
end
if scannedHash == private.prevScannedHash then
Log.Info ( " Hash hasn't changed, so not scanning " )
private.dbPopulated = true
TempTable.Release ( recipes )
TempTable.Release ( prevRecipeIds )
TempTable.Release ( nextRecipeIds )
private.DoneScanning ( scannedHash )
return
end
private.db : TruncateAndBulkInsertStart ( )
private.matDB : TruncateAndBulkInsertStart ( )
local inactiveCraftStrings = TempTable.Acquire ( )
for index , spellId in ipairs ( recipes ) do
local info = private.recipeInfoCache [ spellId ]
local nextSpellId = nextRecipeIds [ spellId ]
local hasHigherRank = nextSpellId and private.recipeInfoCache [ nextSpellId ] and private.recipeInfoCache [ nextSpellId ] . learned
local rank = - 1
if prevRecipeIds [ spellId ] or nextSpellId then
rank = 1
local tempSpellId = spellId
while prevRecipeIds [ tempSpellId ] do
rank = rank + 1
tempSpellId = prevRecipeIds [ tempSpellId ]
end
end
-- There's a Blizzard bug where First Aid duplicates spellIds, so check that this is the right index
-- TODO: show unlearned recipes in the TSM UI
if info and info.index == index and info.learned and not hasHigherRank then
local unlockedLevel = info.unlockedRecipeLevel
local numSkillUps = info.relativeDifficulty == Enum.TradeskillRelativeDifficulty . Optimal and info.numSkillUps or 1
if unlockedLevel then
for level = 1 , MAX_CRAFT_LEVEL do
local craftString = CraftString.Get ( spellId , rank , level )
-- Remove any old version of the spell without a level
inactiveCraftStrings [ CraftString.Get ( spellId ) ] = true
if level <= unlockedLevel then
local recipeScanResult , matScanResult = private.BulkInsertRecipe ( craftString , index , info.name , info.categoryID , info.relativeDifficulty , rank , numSkillUps , level , info.currentRecipeExperience or - 1 , info.nextLevelRecipeExperience or - 1 )
haveInvalidRecipes = haveInvalidRecipes or not recipeScanResult
haveInvalidMats = haveInvalidMats or not matScanResult
else
-- This level isn't unlocked yet
inactiveCraftStrings [ craftString ] = true
end
end
else
local craftString = CraftString.Get ( spellId , rank )
local numResultItems = nil
local indirectResult = ProfessionInfo.GetIndirectCraftResult ( spellId )
if indirectResult then
if type ( indirectResult ) == " table " then
numResultItems = # indirectResult
else
numResultItems = 1
end
else
local result = C_TradeSkillUI.GetRecipeQualityItemIDs ( spellId )
if result then
numResultItems = # result
else
if ItemString.GetBase ( C_TradeSkillUI.GetRecipeItemLink ( spellId ) ) then
local ilvlBonuses = info.qualityIlvlBonuses
if ilvlBonuses then
numResultItems = # ilvlBonuses
else
numResultItems = 1
end
else
numResultItems = 1
end
end
end
if not info.supportsQualities or info.isSalvageRecipe then
assert ( numResultItems == 1 )
local recipeScanResult , matScanResult = private.BulkInsertRecipe ( craftString , index , info.name , info.categoryID , info.relativeDifficulty , rank , numSkillUps , 1 , info.currentRecipeExperience or - 1 , info.nextLevelRecipeExperience or - 1 )
haveInvalidRecipes = haveInvalidRecipes or not recipeScanResult
haveInvalidMats = haveInvalidMats or not matScanResult
elseif numResultItems == 1 then
-- Just ignore this craft for now - this can happen with alchemy experimentation for example
Log.Warn ( " Unexpected single result item (%s, %s) " , tostring ( professionName ) , tostring ( craftString ) )
else
assert ( numResultItems > 1 )
-- This is a quality craft
local recipeDifficulty , baseRecipeQuality , hasQualityMats , inspirationAmount = Scanner.GetRecipeQualityInfo ( craftString )
if baseRecipeQuality then
for i = 1 , numResultItems do
local qualityCraftString = CraftString.Get ( spellId , rank , nil , i )
if Quality.GetNeededSkill ( i , recipeDifficulty , baseRecipeQuality , numResultItems , hasQualityMats , inspirationAmount ) then
local recipeScanResult , matScanResult = private.BulkInsertRecipe ( qualityCraftString , index , info.name , info.categoryID , info.relativeDifficulty , rank , numSkillUps , 1 , info.currentRecipeExperience or - 1 , info.nextLevelRecipeExperience or - 1 )
haveInvalidRecipes = haveInvalidRecipes or not recipeScanResult
haveInvalidMats = haveInvalidMats or not matScanResult
else
-- We can no longer craft this quality
inactiveCraftStrings [ qualityCraftString ] = true
end
end
else
-- Just ignore this craft for now
Log.Warn ( " Could not look up base quality (%s, %s) " , tostring ( professionName ) , tostring ( craftString ) )
end
end
end
end
end
private.matDB : BulkInsertEnd ( )
private.db : BulkInsertEnd ( )
private.dbPopulated = true
if next ( inactiveCraftStrings ) then
private.inactiveFunc ( inactiveCraftStrings )
end
TempTable.Release ( inactiveCraftStrings )
TempTable.Release ( recipes )
TempTable.Release ( prevRecipeIds )
TempTable.Release ( nextRecipeIds )
else
private.PopulateClassicSpellIdLookup ( )
local lastHeaderIndex = 0
private.db : TruncateAndBulkInsertStart ( )
private.matDB : TruncateAndBulkInsertStart ( )
for i = 1 , State.IsClassicCrafting ( ) and GetNumCrafts ( ) or GetNumTradeSkills ( ) do
local name , skillType = nil , nil
if State.IsClassicCrafting ( ) then
local _
name , _ , skillType = GetCraftInfo ( i )
else
name , skillType = GetTradeSkillInfo ( i )
end
if skillType == " header " then
lastHeaderIndex = i
elseif name then
local craftString = CraftString.Get ( private.classicSpellIdLookup [ - i ] )
local recipeScanResult , matScanResult = private.BulkInsertRecipe ( craftString , i , name , lastHeaderIndex , skillType , - 1 , 1 , 1 , - 1 , - 1 )
haveInvalidRecipes = haveInvalidRecipes or not recipeScanResult
haveInvalidMats = haveInvalidMats or not matScanResult
end
end
private.matDB : BulkInsertEnd ( )
private.db : BulkInsertEnd ( )
private.dbPopulated = true
end
if haveInvalidRecipes then
-- We'll try again
private.QueueProfessionScan ( )
return
elseif State.IsNPC ( ) or State.IsLinked ( ) or State.IsGuild ( ) then
-- We don't want to store this profession in our application DB, so we're done
private.DoneScanning ( scannedHash )
return
end
local craftStrings = TempTable.Acquire ( )
private.db : NewQuery ( )
: Select ( " craftString " )
: NotEqual ( " itemString " , " " )
: AsTable ( craftStrings )
: Release ( )
local categorySkillLevelLookup = TempTable.Acquire ( )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
for _ , craftString in ipairs ( craftStrings ) do
local spellId = CraftString.GetSpellId ( craftString )
local categoryId = private.recipeInfoCache [ spellId ] . categoryID
categorySkillLevelLookup [ craftString ] = private.GetCurrentCategorySkillLevel ( categoryId )
end
end
local done , rescan = private.scanHookFunc ( professionName , craftStrings , categorySkillLevelLookup )
TempTable.Release ( craftStrings )
TempTable.Release ( categorySkillLevelLookup )
if rescan then
private.QueueProfessionScan ( )
end
if done and not haveInvalidMats then
private.DoneScanning ( scannedHash )
end
wipe ( private.recipeInfoCache )
end
function private . BulkInsertRecipe ( craftString , index , name , categoryId , relativeDifficulty , rank , numSkillUps , level , currentRecipeExperience , nextLevelRecipeExperience )
local itemString , craftName = private.GetItemStringAndCraftName ( craftString )
if not itemString or not craftName then
return false , false
end
private.db : BulkInsertNewRow ( craftString , itemString , index , name , craftName , categoryId , relativeDifficulty , rank , numSkillUps , level , currentRecipeExperience , nextLevelRecipeExperience )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local spellId = CraftString.GetSpellId ( craftString )
private.recipeInfoCache [ craftString ] = private.recipeInfoCache [ spellId ]
end
local matScanResult = private.BulkInsertMats ( craftString )
return true , matScanResult
end
function private . GetItemStringAndCraftName ( craftString )
-- Get the links
local spellId = CraftString.GetSpellId ( craftString )
local quality = CraftString.GetQuality ( craftString )
local resultItem , indirectSpellId = Scanner.GetResultItem ( craftString )
-- Get the itemString and craft name
local itemString , craftName = nil , nil
if quality then
assert ( type ( resultItem ) == " table " )
assert ( resultItem [ quality ] )
itemString = ItemString.ToLevel ( ItemString.Get ( resultItem [ quality ] ) )
craftName = ItemInfo.GetName ( itemString )
elseif strfind ( resultItem , " enchant: " ) then
itemString = " "
craftName = GetSpellInfo ( indirectSpellId or spellId )
elseif strfind ( resultItem , " item: " ) then
-- Result of craft is item
local level = CraftString.GetLevel ( craftString )
if level and level > 0 then
local relLevel = ProfessionInfo.GetRelativeItemLevelByRank ( level )
local baseItemString = ItemString.GetBase ( resultItem )
itemString = baseItemString .. ( relLevel < 0 and " ::- " or " ::+ " ) .. abs ( relLevel )
else
itemString = ItemString.GetBase ( resultItem )
end
craftName = ItemInfo.GetName ( resultItem )
-- 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 ( craftString ) )
end
if not itemString or not craftName then
Log.Warn ( " No itemString (%s) or craftName (%s) found (%s) " , tostring ( itemString ) , tostring ( craftName ) , tostring ( craftString ) )
return nil , nil
end
return itemString , craftName
end
function private . BulkInsertMats ( craftString )
wipe ( private.matQuantitiesTemp )
local haveInvalidMats = false
for i = 1 , Scanner.GetNumMats ( craftString ) do
local matItemString , quantity , name , isQualityMat = Scanner.GetMatInfo ( craftString , i )
if not matItemString then
local professionName = State.GetCurrentProfession ( )
Log.Warn ( " Failed to get itemString for mat %d (%s, %s) " , i , tostring ( professionName ) , tostring ( craftString ) )
haveInvalidMats = true
break
end
if not name or not quantity then
local professionName = State.GetCurrentProfession ( )
Log.Warn ( " Failed to get name (%s) or quantity (%s) for mat (%s, %s, %d) " , tostring ( name ) , tostring ( quantity ) , tostring ( professionName ) , tostring ( craftString ) , i )
haveInvalidMats = true
break
end
if not isQualityMat then
ItemInfo.StoreItemName ( matItemString , name )
private.matQuantitiesTemp [ matItemString ] = quantity
end
end
if Scanner.IsEnchant ( craftString ) and Environment.IsWrathClassic ( ) then
-- Add a vellum to the list of mats
local vellumItemString = Scanner.GetVellumItemString ( craftString )
if vellumItemString then
private.matQuantitiesTemp [ vellumItemString ] = 1
else
haveInvalidMats = true
end
end
if haveInvalidMats then
return false
end
for matString , quantity in pairs ( private.matQuantitiesTemp ) do
private.matDB : BulkInsertNewRow ( craftString , matString , quantity , " " )
end
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local categorySkillLevel = private.GetCurrentCategorySkillLevel ( private.recipeInfoCache [ craftString ] . categoryID )
local spellId = CraftString.GetSpellId ( craftString )
local level = CraftString.GetLevel ( craftString )
local info = C_TradeSkillUI.GetRecipeSchematic ( spellId , false , level )
if info.recipeType == Enum.TradeskillRecipeType . Salvage then
local matString = MatString.Create ( MatString.TYPE . REQUIRED , 1 , C_TradeSkillUI.GetSalvagableItemIDs ( CraftString.GetSpellId ( craftString ) ) )
private.matDB : BulkInsertNewRow ( craftString , matString , info.quantityMin , " " )
else
for _ , data in ipairs ( info.reagentSlotSchematics ) do
if private.IsSpecialMatValid ( data , categorySkillLevel ) then
assert ( not next ( private.matStringItemsTemp ) )
for _ , craftingReagent in ipairs ( data.reagents ) do
tinsert ( private.matStringItemsTemp , craftingReagent.itemID )
end
local matStringType , slotText = nil , nil
if data.reagentType == Enum.CraftingReagentType . Basic and data.dataSlotType == Enum.TradeskillSlotDataType . ModifiedReagent then
matStringType = MatString.TYPE . QUALITY
slotText = ItemInfo.GetName ( " i: " .. data.reagents [ 1 ] . itemID ) or " "
elseif data.reagentType == Enum.CraftingReagentType . Optional or data.reagentType == Enum.CraftingReagentType . Modifying then
if data.required then
matStringType = MatString.TYPE . REQUIRED
slotText = data.slotInfo . slotText or REQUIRED_REAGENT_TOOLTIP_CLICK_TO_ADD
else
matStringType = MatString.TYPE . OPTIONAL
slotText = data.slotInfo . slotText or OPTIONAL_REAGENT_POSTFIX
end
elseif data.reagentType == Enum.CraftingReagentType . Finishing then
matStringType = MatString.TYPE . FINISHING
slotText = data.slotInfo . slotText or OPTIONAL_REAGENT_POSTFIX
else
error ( " Unexpected optional mat type: " .. tostring ( data.reagentType ) .. " , " .. tostring ( data.dataSlotType ) )
end
local matString = MatString.Create ( matStringType , data.dataSlotIndex , private.matStringItemsTemp )
wipe ( private.matStringItemsTemp )
private.matDB : BulkInsertNewRow ( craftString , matString , data.quantityRequired , slotText )
end
end
end
end
return true
end
function private . DoneScanning ( scannedHash )
private.prevScannedHash = scannedHash
if not private.hasScanned then
private.hasScanned = true
for _ , callback in ipairs ( private.callbacks ) do
callback ( )
end
end
end
function private . ClearFilters ( )
if Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) then
local hadFilter = false
if C_TradeSkillUI.GetShowUnlearned ( ) then
C_TradeSkillUI.SetShowLearned ( true )
C_TradeSkillUI.SetShowUnlearned ( 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
return hadFilter
else
-- TODO
return false
end
end
function private . PopulateClassicSpellIdLookup ( )
assert ( not Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) )
assert ( State.GetCurrentProfession ( ) and State.IsDataStable ( ) )
wipe ( private.classicSpellIdLookup )
for i = 1 , State.IsClassicCrafting ( ) and GetNumCrafts ( ) or GetNumTradeSkills ( ) do
local hash = private.GetClassicSpellId ( i )
if hash then
assert ( hash >= 0 and not private.classicSpellIdLookup [ hash ] and not private.classicSpellIdLookup [ - i ] )
private.classicSpellIdLookup [ hash ] = i
private.classicSpellIdLookup [ - i ] = hash
end
end
end
function private . GetClassicSpellId ( index )
local name , skillType = nil , nil
if State.IsClassicCrafting ( ) then
local _
name , _ , skillType = GetCraftInfo ( index )
else
name , skillType = GetTradeSkillInfo ( index )
end
if skillType == " header " or not name then
return
end
local hash = Math.CalculateHash ( name )
if State.IsClassicCrafting ( ) then
hash = Math.CalculateHash ( GetCraftIcon ( index ) , hash )
for i = 1 , GetCraftNumReagents ( index ) do
local _ , _ , quantity = GetCraftReagentInfo ( index , i )
hash = Math.CalculateHash ( ItemString.Get ( GetCraftReagentItemLink ( index , i ) ) , hash )
hash = Math.CalculateHash ( quantity , hash )
end
else
hash = Math.CalculateHash ( GetTradeSkillIcon ( index ) , hash )
for i = 1 , GetTradeSkillNumReagents ( index ) do
local _ , _ , quantity = GetTradeSkillReagentInfo ( index , i )
hash = Math.CalculateHash ( ItemString.Get ( GetTradeSkillReagentItemLink ( index , i ) ) , hash )
hash = Math.CalculateHash ( quantity , hash )
end
end
return hash
end
function private . GetCurrentCategorySkillLevel ( categoryId )
if private.categorySkillLevelCache . lastUpdate ~= GetTime ( ) then
wipe ( private.categorySkillLevelCache )
private.categorySkillLevelCache . lastUpdate = GetTime ( )
end
if not private.categorySkillLevelCache [ categoryId ] then
local categoryInfo = C_TradeSkillUI.GetCategoryInfo ( categoryId )
while not categoryInfo.skillLineCurrentLevel and categoryInfo.parentCategoryID do
categoryInfo = C_TradeSkillUI.GetCategoryInfo ( categoryInfo.parentCategoryID )
end
private.categorySkillLevelCache [ categoryId ] = categoryInfo.skillLineCurrentLevel or 0
end
return private.categorySkillLevelCache [ categoryId ]
end
function private . IsSpecialMatValid ( data , categorySkillLevel )
if data.reagentType == Enum.CraftingReagentType . Modifying and data.required then
return true
end
if data.reagentType == Enum.CraftingReagentType . Basic and data.dataSlotType == Enum.TradeskillSlotDataType . ModifiedReagent then
-- pass
elseif data.reagentType == Enum.CraftingReagentType . Optional or data.reagentType == Enum.CraftingReagentType . Modifying or data.reagentType == Enum.CraftingReagentType . Finishing then
-- pass
else
return false
end
return data.slotInfo . requiredSkillRank <= categorySkillLevel
end
function private . IsRegularMat ( data )
assert ( Environment.HasFeature ( Environment.FEATURES . C_TRADE_SKILL_UI ) )
if data.reagentType == Enum.CraftingReagentType . Modifying and data.required then
return true
end
if data.reagentType ~= Enum.CraftingReagentType . Basic then
return false
end
return data.dataSlotType == Enum.TradeskillSlotDataType . Reagent or data.dataSlotType == Enum.TradeskillSlotDataType . ModifiedReagent
end