From 3662193dda4ff87d12352aaaa0016673588d8509 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 02:15:31 +0200 Subject: [PATCH 1/3] fix(catalog): never tooltip-scan advancement IDs as spell IDs Entries with no backing spell (spellId == 0) fell through to SetHyperlink("spell:" .. entry.ID), aliasing CoA advancement IDs into real 3.3.5 spell-ID space - ~3800 rows captured an unrelated spell's tooltip and adopted its first line as the entry NAME. Spell-less entries now get an empty tooltip and keep the "ID:" name. --- CoaExporter/Catalogs/Common.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CoaExporter/Catalogs/Common.lua b/CoaExporter/Catalogs/Common.lua index 0de7efc..d1a7e77 100644 --- a/CoaExporter/Catalogs/Common.lua +++ b/CoaExporter/Catalogs/Common.lua @@ -166,9 +166,17 @@ function C.Run(filter, callback) end end + -- Only tooltip-scan entries with a backing spell. Advancement + -- IDs are NOT spell IDs - SetHyperlink("spell:" .. entry.ID) + -- would alias them into real 3.3.5 spell-ID space and capture + -- some unrelated spell's tooltip (and adopt its first line as + -- the entry name). local name, _, icon - if spellId > 0 then name, _, icon = GetSpellInfo(spellId) end - local tooltip = GetCatalogTooltip(spellId > 0 and spellId or entry.ID) + local tooltip = "" + if spellId > 0 then + name, _, icon = GetSpellInfo(spellId) + tooltip = GetCatalogTooltip(spellId) + end if not name or name == "" then name = tooltip:match("^([^\n]+)") or ("ID:" .. tostring(entry.ID)) end From f5be9f0102e622356ebff31f7000adc71d17d8d6 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 02:15:57 +0200 Subject: [PATCH 2/3] fix(scrolls): don't cache scrolls whose tooltip scraped empty TryResolve cached an entry even when ScanItemTooltip returned 0 lines (empty string is truthy in Lua), so empty-tooltip scrolls were treated as cache hits forever and never retried - and per-pass stats disagreed with the cache. Only cache once at least one tooltip line was captured, and treat cached-but-empty entries (stale SavedVariables) as misses. --- CoaExporter/Collectors/MysticScrolls.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CoaExporter/Collectors/MysticScrolls.lua b/CoaExporter/Collectors/MysticScrolls.lua index a8fbd23..df6e97c 100644 --- a/CoaExporter/Collectors/MysticScrolls.lua +++ b/CoaExporter/Collectors/MysticScrolls.lua @@ -71,7 +71,8 @@ end local function TryResolve(entry) local cache = CoaExporterScrollCache.entries - if cache[entry.itemID] and cache[entry.itemID].itemTooltip then + local hit = cache[entry.itemID] + if hit and hit.itemTooltip and hit.itemTooltip ~= "" then return true end local name, _, quality = GetItemInfo(entry.itemID) @@ -82,6 +83,11 @@ local function TryResolve(entry) return false end local itemLines = ScanItemTooltip(entry.itemID) + if #itemLines == 0 then + -- Tooltip not server-resolved yet; don't cache an empty entry, or + -- this scroll would never be retried on later passes. + return false + end local spellName, spellRank if GetItemSpell then spellName, spellRank = GetItemSpell(entry.itemID) @@ -97,7 +103,7 @@ local function TryResolve(entry) itemTooltip = table.concat(itemLines, "\n"), fetchedAt = time(), } - return #itemLines > 0 + return true end function AE.ScrollsScanPass() From 53bb5f6a85941ab206421093c4c866dcffccf65d Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 02:16:33 +0200 Subject: [PATCH 3/3] chore: scanner-tooltip pokes, honest pass stats, drop dead code, ISO timestamps - Items/MysticScrolls: use the dedicated hidden scanner tooltip for the server-query poke instead of the global GameTooltip - Items: count cache hits and this-pass resolutions separately so the pass 2+ chat output no longer claims old hits as new - Core: remove dead tiny_json_encode fallback (Util/Json.lua loads first in the TOC and always defines CoaExporter_Json_Encode) - Catalogs/Common: ctx.startedAt now ISO 8601 UTC like every other exporter timestamp --- CoaExporter/Catalogs/Common.lua | 2 +- CoaExporter/Collectors/Items.lua | 26 ++++++++++++++---------- CoaExporter/Collectors/MysticScrolls.lua | 6 +++--- CoaExporter/Core.lua | 26 +----------------------- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/CoaExporter/Catalogs/Common.lua b/CoaExporter/Catalogs/Common.lua index d1a7e77..de9e609 100644 --- a/CoaExporter/Catalogs/Common.lua +++ b/CoaExporter/Catalogs/Common.lua @@ -100,7 +100,7 @@ function C.Run(filter, callback) playerClassFile = playerClassFile or "UNKNOWN" local ctx = { - startedAt = date(), + startedAt = date("!%Y-%m-%dT%H:%M:%SZ"), filter = filter or "all", playerClass = playerLocalizedClass, playerClassFile = playerClassFile, diff --git a/CoaExporter/Collectors/Items.lua b/CoaExporter/Collectors/Items.lua index e549572..836b851 100644 --- a/CoaExporter/Collectors/Items.lua +++ b/CoaExporter/Collectors/Items.lua @@ -88,11 +88,9 @@ local function TryResolve(itemID) local name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, sellPrice = GetItemInfo(itemID) if not name then - -- Trigger the server-side ITEM_QUERY_SINGLE; this is also what writes - -- the row into itemcache.wdb. - GameTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - GameTooltip:SetHyperlink("item:" .. itemID) - GameTooltip:Hide() + -- Trigger the server-side ITEM_QUERY_SINGLE via the hidden scanner + -- tooltip; this is also what writes the row into itemcache.wdb. + ScanItemTooltip(itemID) return false end local itemLines = ScanItemTooltip(itemID) @@ -121,22 +119,28 @@ local function TryResolve(itemID) return true end --- One synchronous pass over `ids`. Returns counts + the still-unresolved list. +-- One synchronous pass over `ids`. Returns counts + the still-unresolved +-- list. `resolved` counts everything in the cache (including hits from +-- earlier passes); `new` counts only ids that resolved during this pass. function AE.ItemsScanPass(ids) + local cache = CoaExporterItemCache.entries local total = #ids + local resolved = 0 local newlyResolved = 0 local unresolved = {} for _, id in ipairs(ids) do + local wasCached = cache[id] and cache[id].name ~= nil if TryResolve(id) then - newlyResolved = newlyResolved + 1 + resolved = resolved + 1 + if not wasCached then newlyResolved = newlyResolved + 1 end else unresolved[#unresolved+1] = id end end CoaExporterItemCache.meta.lastScanAt = time() - CoaExporterItemCache.meta.resolved = newlyResolved + CoaExporterItemCache.meta.resolved = resolved CoaExporterItemCache.meta.unresolved = unresolved - return { total = total, resolved = newlyResolved, unresolved = #unresolved } + return { total = total, resolved = resolved, new = newlyResolved, unresolved = #unresolved } end -- Multi-pass scan with delays so the server has time to answer queries. @@ -147,8 +151,8 @@ function AE.ItemsStartScan(ids, callback) attempts = attempts + 1 local stats = AE.ItemsScanPass(ids) DEFAULT_CHAT_FRAME:AddMessage(string.format( - "CoaExporter items: pass %d/%d - resolved this pass %d/%d (still unresolved: %d)", - attempts, maxAttempts, stats.resolved, stats.total, stats.unresolved)) + "CoaExporter items: pass %d/%d - resolved %d/%d (%d new this pass, still unresolved: %d)", + attempts, maxAttempts, stats.resolved, stats.total, stats.new, stats.unresolved)) if stats.unresolved > 0 and attempts < maxAttempts then local t = CreateFrame("Frame") local elapsed = 0 diff --git a/CoaExporter/Collectors/MysticScrolls.lua b/CoaExporter/Collectors/MysticScrolls.lua index df6e97c..fdcac0f 100644 --- a/CoaExporter/Collectors/MysticScrolls.lua +++ b/CoaExporter/Collectors/MysticScrolls.lua @@ -77,9 +77,9 @@ local function TryResolve(entry) end local name, _, quality = GetItemInfo(entry.itemID) if not name then - GameTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - GameTooltip:SetHyperlink("item:" .. entry.itemID) - GameTooltip:Hide() + -- Poke the server (ITEM_QUERY_SINGLE) via the hidden scanner so the + -- next pass can resolve - no need to touch the global GameTooltip. + ScanItemTooltip(entry.itemID) return false end local itemLines = ScanItemTooltip(entry.itemID) diff --git a/CoaExporter/Core.lua b/CoaExporter/Core.lua index af90b89..32248ef 100644 --- a/CoaExporter/Core.lua +++ b/CoaExporter/Core.lua @@ -128,31 +128,7 @@ end function AE:Export(which) local normWhich = Norm(which) local data = self:AssembleExport(normWhich) - local function tiny_json_encode(v) - local tv = type(v) - if tv == 'nil' then return 'null' end - if tv == 'boolean' then return v and 'true' or 'false' end - if tv == 'number' then return tostring(v) end - if tv == 'string' then - local s = v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n'):gsub('\r', '\\r'):gsub('\t', '\\t') - return '"' .. s .. '"' - end - if tv == 'table' then - local n = 0 - for k,_ in pairs(v) do if type(k) ~= 'number' then n = -1 break else n = math.max(n, k) end end - if n >= 1 then - local parts = {} - for i=1,n do parts[#parts+1] = tiny_json_encode(v[i]) end - return '[' .. table.concat(parts, ',') .. ']' - else - local parts = {} - for k,val in pairs(v) do parts[#parts+1] = tiny_json_encode(tostring(k)) .. ':' .. tiny_json_encode(val) end - return '{' .. table.concat(parts, ',') .. '}' - end - end - return 'null' - end - local encoder = _G.CoaExporter_Json_Encode or tiny_json_encode + local encoder = _G.CoaExporter_Json_Encode local json = encoder(data) local title = "CoaExporter"