From c199a1d5bd728d90d13e2a99fa73835d87f52080 Mon Sep 17 00:00:00 2001 From: Uga <5999549+uga@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:11:06 +0200 Subject: [PATCH 1/3] Store the recipes whose difficulty the client does not color ScanRecipes_NonRetail() classified every line of the trade skill window through SkillTypeToColor, and stored it only if the lookup succeeded: local color = SkillTypeToColor[skillType] if color then ... crafts[i] = format("%s|%s", color, craftInfo) end The table knew header, optimal, medium, easy and trivial. GetTradeSkillInfo() returns two more. Blizzard's own TradeSkillTypeColor lists "nodifficulty", drawn in white, which is what the client uses for a recipe that can never grant a skill up - in practice mostly the epic patterns and formulas. And "subheader", which the same function already expects a few lines above, when it tests the first line of the list: or (skillType ~= "header" and skillType ~= "subheader") then Neither had a color, so neither was stored, and the recipe simply never existed as far as Altoholic was concerned. Dropping an entry does more than lose it. Crafts is indexed by the position of the line in the window, so a skipped entry leaves a hole, and the length of a table with a hole is any border Lua cares to return. Both readers walk it with "for i = 1, #crafts", so a single white recipe can also truncate every recipe after it. _IterateRecipes() carries a "Somehow the scan can set an item to nil" guard which is that symptom, worked around at the far end. Unknown types now fall back to grey rather than being dropped, so a type added by a future patch costs at worst a wrong color, never a missing recipe and never a hole. Grey is also the honest bucket for the two being named here: a recipe the client refuses to color is one that grants no skill up. This is not reproduced. It is the shape of a report of epic recipes missing from tailoring and enchanting on Mists, where a maxed profession has them, and it is a defect on its own terms either way. Co-Authored-By: Claude Opus 5 --- DataStore_Crafts/DataStore_Crafts_Retail.lua | 43 +++++++++++--------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/DataStore_Crafts/DataStore_Crafts_Retail.lua b/DataStore_Crafts/DataStore_Crafts_Retail.lua index 3d5c0b3..04e8c39 100644 --- a/DataStore_Crafts/DataStore_Crafts_Retail.lua +++ b/DataStore_Crafts/DataStore_Crafts_Retail.lua @@ -321,12 +321,16 @@ local function ScanProfessionLinks() AddonFactory:Broadcast("DATASTORE_PROFESSION_LINKS_UPDATED") end +local SKILL_GREY = 4 + local SkillTypeToColor = { ["header"] = 0, + ["subheader"] = 0, -- lists are grouped by header, and sometimes by subheader ["optimal"] = 1, -- orange ["medium"] = 2, -- yellow ["easy"] = 3, -- green ["trivial"] = 4, -- grey + ["nodifficulty"] = 4, -- white, ie: recipes that never grant a skill up, mostly the epic ones. Grey is the closest match. } local function ScanCooldowns() @@ -570,30 +574,31 @@ local function ScanRecipes_NonRetail() end -- Scan recipe - local color = SkillTypeToColor[skillType] + -- An unknown skill type must still be stored: dropping it not only loses the recipe, + -- it leaves a hole in the crafts array, and the readers stop at the first hole. + local color = SkillTypeToColor[skillType] or SKILL_GREY local craftInfo - if color then - if skillType == "header" then - craftInfo = skillName or "" - TableInsert(profession.Categories, skillName) - else - -- cooldowns, if any - local cooldown = GetTradeSkillCooldown(i) - if cooldown then - -- ex: "Hexweave Cloth|86220|1533539676" expire at "now + cooldown" - TableInsert(profession.Cooldowns, format("%s|%d|%d", skillName, cooldown, cooldown + time())) - end + if color == 0 then + craftInfo = skillName or "" + TableInsert(profession.Categories, skillName) + else + -- cooldowns, if any + local cooldown = GetTradeSkillCooldown(i) + if cooldown then + -- ex: "Hexweave Cloth|86220|1533539676" expire at "now + cooldown" + TableInsert(profession.Cooldowns, format("%s|%d|%d", skillName, cooldown, cooldown + time())) + end - -- if there is a valid recipeID, save it - if recipeLink then - craftInfo = (recipeLink and recipeID) and recipeID or "" - else - craftInfo = (link and itemID) and itemID or "" - end + -- if there is a valid recipeID, save it + if recipeLink then + craftInfo = (recipeLink and recipeID) and recipeID or "" + else + craftInfo = (link and itemID) and itemID or "" end - crafts[i] = format("%s|%s", color, craftInfo) end + + crafts[i] = format("%s|%s", color, craftInfo) end -- Old school enchanting From 348da45a345eb30ad4b7fe1d813eff46a487604b Mon Sep 17 00:00:00 2001 From: Uga <5999549+uga@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:11:20 +0200 Subject: [PATCH 2/3] Read each recipe's id independently of the previous entry Two defects in the same few lines, both about what happens when a line of the trade skill window does not yield the link the code expects. recipeID is declared once, above the loop, and only ever assigned inside "if recipeLink then". An entry whose link carries no enchant id therefore kept the id of whatever entry was scanned before it, and the leftover was used twice over: to key the crafted item resultItemsDB[recipeID] = maxMade + bit64:LeftShift(itemID, 8) and to store the recipe itself, as "craftInfo = (recipeLink and recipeID) and recipeID or """. The result is a recipe recorded under another recipe's id, which reads back as a duplicate of its neighbour. It is now reset at the top of every iteration, so a missing id stays missing. The id was also extracted in two steps, and the first one was unchecked: local found, _, enchantString = string.find(recipeLink, "^|%x+|H(.+)|h%[.+%]") recipeID = tonumber(enchantString:match("enchant:(%d+)")) A link that does not match the pattern leaves enchantString nil and the next line throws. The scan wipes Crafts before the loop, so an error part way through does not just skip that entry, it leaves the profession holding however much had been read before it - and the window has to be reopened to try again. Matching the id directly against the link cannot throw, and returns exactly the same value for the normal "|Henchant:12345|h[Name]|h" form. Co-Authored-By: Claude Opus 5 --- DataStore_Crafts/DataStore_Crafts_Retail.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DataStore_Crafts/DataStore_Crafts_Retail.lua b/DataStore_Crafts/DataStore_Crafts_Retail.lua index 04e8c39..5528e0e 100644 --- a/DataStore_Crafts/DataStore_Crafts_Retail.lua +++ b/DataStore_Crafts/DataStore_Crafts_Retail.lua @@ -532,14 +532,15 @@ local function ScanRecipes_NonRetail() end -- Get recipeID - + + recipeID = nil -- reset it, or an entry without a valid link would inherit the previous entry's id recipeLink = GetTradeSkillRecipeLink(i) -- add recipe link here to get recipeID if not recipeLink then recipeLink = GetCraftRecipeLink(i) end if recipeLink then - local found, _, enchantString = string.find(recipeLink, "^|%x+|H(.+)|h%[.+%]") - recipeID = tonumber(enchantString:match("enchant:(%d+)")) + -- an unexpected link format must not throw, or the rest of the list would not be scanned at all + recipeID = tonumber(recipeLink:match("|Henchant:(%d+)")) if recipeID then reagentsDB[recipeID] = TableConcat(reagentsInfo, "|") end From c27405435d09d3ff9ce85dd09e8876721c40b961 Mon Sep 17 00:00:00 2001 From: Uga <5999549+uga@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:11:37 +0200 Subject: [PATCH 3/3] Count non-retail recipes by their own color index _GetNumRecipesByColor() was written for retail and registered for every flavour, but the two _IterateRecipes() implementations do not hand the same thing to their callback. Retail passes one bit packed number per recipe, non-retail passes the color index, the id and the position, and calls back for recipes only, never for headers. So on non-retail the color index went through _GetRecipeInfo(), which read it as packed data and returned its two low bits: 1 orange -> 1 3 green -> 3 2 yellow -> 2 4 grey -> 0 then the counters were read back in retail's order, counts[3] first as the orange one. Orange and green were reported swapped: the Summary tooltip of a leatherworker with 9 orange, 6 yellow, 4 green and 131 grey recipes announced 4 orange and 9 green. The total was right, which is presumably why this survived - it is the sum of the same four counters either way, and grey landed on counts[0] by the same accident that moved the others. The split this restores is the one the pre-merge DataStore_Crafts.lua still carries, as _GetNumRecipesByColor_Retail and _GetNumRecipesByColor_NonRetail; the merged file kept the retail one only. Verified in game against the same character: 150 recipes, 4 green, 6 yellow, 9 orange. Co-Authored-By: Claude Opus 5 --- DataStore_Crafts/DataStore_Crafts_Retail.lua | 21 +++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/DataStore_Crafts/DataStore_Crafts_Retail.lua b/DataStore_Crafts/DataStore_Crafts_Retail.lua index 5528e0e..756008b 100644 --- a/DataStore_Crafts/DataStore_Crafts_Retail.lua +++ b/DataStore_Crafts/DataStore_Crafts_Retail.lua @@ -919,15 +919,26 @@ end local function _GetNumRecipesByColor(profession) -- counts the number of orange, yellow, green and grey recipes. - local counts = { [0] = 0, [1] = 0, [2] = 0, [3] = 0 } - - _IterateRecipes(profession, 0, 0, function(recipeData) + local counts = { [0] = 0, [1] = 0, [2] = 0, [3] = 0, [4] = 0 } + + -- Non-retail hands the color index straight to the callback, and does not call back for headers. + if not isRetail then + _IterateRecipes(profession, 0, 0, function(color) + if color then + counts[color] = (counts[color] or 0) + 1 + end + end) + + return counts[1], counts[2], counts[3], counts[4] -- orange, yellow, green, grey + end + + _IterateRecipes(profession, 0, 0, function(recipeData) if recipeData then local color = _GetRecipeInfo(recipeData) - counts[color] = counts[color] + 1 + counts[color] = (counts[color] or 0) + 1 end end) - + return counts[3], counts[2], counts[1], counts[0] -- orange, yellow, green, grey end