Compare commits

...

3 commits

Author SHA1 Message Date
53bb5f6a85 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
2026-06-10 02:16:33 +02:00
f5be9f0102 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.
2026-06-10 02:15:57 +02:00
3662193dda 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:<id>" name.
2026-06-10 02:15:31 +02:00
4 changed files with 38 additions and 44 deletions

View file

@ -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,
@ -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

View file

@ -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

View file

@ -71,17 +71,23 @@ 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)
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)
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()

View file

@ -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"