- 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
235 lines
8.4 KiB
Lua
235 lines
8.4 KiB
Lua
-- CoaExporter / Collectors / Items.lua
|
|
--
|
|
-- Async item-ID scanner. Forces the client to fetch item templates from the
|
|
-- server for a list/range of item IDs, captures the resolved data, and (as a
|
|
-- side effect) populates the client's itemcache.wdb so a server-side
|
|
-- re-import can pick the items up too.
|
|
--
|
|
-- Why this exists: CoA's per-class character-creation starting gear lives at
|
|
-- item ids 2000000+. The client only caches an item once a character has
|
|
-- actually seen it in-game, so items nobody has viewed are absent from both
|
|
-- the cache and db.exil.es. Calling GetItemInfo()/SetHyperlink() on an
|
|
-- unseen id triggers ITEM_QUERY_SINGLE to the server, which fills the cache.
|
|
--
|
|
-- Usage:
|
|
-- /coae items scan → scan the default CoA custom block
|
|
-- /coae items scan missing → scan the known-missing id list (below)
|
|
-- /coae items scan 2000000 2000200 → scan an explicit inclusive range
|
|
-- /coae items scan 2089618 → scan a single id
|
|
-- /coae items export → JSON of everything resolved so far
|
|
-- /coae items status → resolved/seen counts
|
|
-- /coae items reset → clear the cache
|
|
--
|
|
-- Design mirrors Collectors/MysticScrolls.lua: a pass fires a batch of
|
|
-- GetItemInfo requests, waits ~5s, then re-scans and records whatever
|
|
-- resolved; unresolved ids are retried up to maxAttempts. Results accumulate
|
|
-- in the CoaExporterItemCache SavedVariable across sessions.
|
|
|
|
CoaExporter = _G.CoaExporter or {}
|
|
_G.CoaExporter = CoaExporter
|
|
local AE = CoaExporter
|
|
|
|
CoaExporterItemCache = CoaExporterItemCache or {
|
|
entries = {},
|
|
meta = { lastScanAt = nil, lastRange = nil, resolved = 0, unresolved = {} },
|
|
}
|
|
|
|
-- Default scan target: the CoA character-creation block plus the one known
|
|
-- outlier (2089618). Range is inclusive. Keep this aligned with the gaps the
|
|
-- db.exil.es item_character_creation mapping is still missing.
|
|
AE.ItemsDefaultFrom = 2000000
|
|
AE.ItemsDefaultTo = 2000099
|
|
AE.ItemsExtras = { 2089618 }
|
|
|
|
-- Known-missing ids (absent from db.exil.es as of 2026-06-06). `/coae items
|
|
-- scan missing` targets exactly these.
|
|
AE.ItemsMissing = {
|
|
2000003, 2000006,
|
|
2000027, 2000028, 2000029, 2000030,
|
|
2000034, 2000035, 2000036, 2000037,
|
|
2000048, 2000049, 2000050,
|
|
2089618,
|
|
}
|
|
|
|
local function EnsureScanner()
|
|
if not AE._itemScanner then
|
|
AE._itemScanner = CreateFrame("GameTooltip", "CoaExpItemsTT", nil, "GameTooltipTemplate")
|
|
AE._itemScanner:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
end
|
|
return AE._itemScanner
|
|
end
|
|
|
|
local function TipLines(tt)
|
|
local out = {}
|
|
for i = 1, tt:NumLines() do
|
|
local left = _G[tt:GetName() .. "TextLeft" .. i]
|
|
if left then
|
|
local t = left:GetText()
|
|
if t and t ~= "" then out[#out+1] = t end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function ScanItemTooltip(itemID)
|
|
local tt = EnsureScanner()
|
|
tt:ClearLines()
|
|
tt:SetHyperlink("item:" .. itemID)
|
|
return TipLines(tt)
|
|
end
|
|
|
|
-- Returns true when the item is resolved (now or already cached). On a miss
|
|
-- it pokes the server (tooltip hyperlink) so the next pass can pick it up.
|
|
local function TryResolve(itemID)
|
|
local cache = CoaExporterItemCache.entries
|
|
if cache[itemID] and cache[itemID].name then
|
|
return true
|
|
end
|
|
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 via the hidden scanner
|
|
-- tooltip; this is also what writes the row into itemcache.wdb.
|
|
ScanItemTooltip(itemID)
|
|
return false
|
|
end
|
|
local itemLines = ScanItemTooltip(itemID)
|
|
local spellName, spellRank
|
|
if GetItemSpell then
|
|
spellName, spellRank = GetItemSpell(itemID)
|
|
end
|
|
cache[itemID] = {
|
|
itemID = itemID,
|
|
name = name,
|
|
link = link,
|
|
quality = quality,
|
|
itemLevel = iLevel,
|
|
reqLevel = reqLevel,
|
|
class = class, -- localized item class ("Armor", "Weapon")
|
|
subClass = subclass, -- localized subclass ("Cloth", "Polearm")
|
|
maxStack = maxStack,
|
|
equipSlot = equipSlot, -- INVTYPE_* string
|
|
icon = texture and ((texture:match("[Ii]nterface\\[Ii]cons\\(.+)") or texture):lower()) or nil,
|
|
sellPrice = sellPrice, -- nil on stock 3.3.5; present on Ascension's client
|
|
spellName = spellName,
|
|
spellRank = spellRank,
|
|
tooltip = table.concat(itemLines, "\n"),
|
|
fetchedAt = time(),
|
|
}
|
|
return true
|
|
end
|
|
|
|
-- 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
|
|
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 = resolved
|
|
CoaExporterItemCache.meta.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.
|
|
function AE.ItemsStartScan(ids, callback)
|
|
local attempts = 0
|
|
local maxAttempts = 5
|
|
local function step()
|
|
attempts = attempts + 1
|
|
local stats = AE.ItemsScanPass(ids)
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"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
|
|
t:SetScript("OnUpdate", function(self, dt)
|
|
elapsed = elapsed + dt
|
|
if elapsed > 5 then
|
|
self:SetScript("OnUpdate", nil)
|
|
step()
|
|
end
|
|
end)
|
|
else
|
|
if stats.unresolved > 0 then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"CoaExporter items: %d id(s) never resolved - likely no server-side template (dead ids).",
|
|
stats.unresolved))
|
|
end
|
|
if callback then callback(stats) end
|
|
end
|
|
end
|
|
step()
|
|
end
|
|
|
|
-- Build the id list for a `/coae items scan <args>` invocation.
|
|
function AE.ItemsResolveTargets(arg)
|
|
arg = arg and arg:match("^%s*(.-)%s*$") or ""
|
|
if arg == "" then
|
|
local ids = {}
|
|
for id = AE.ItemsDefaultFrom, AE.ItemsDefaultTo do ids[#ids+1] = id end
|
|
for _, id in ipairs(AE.ItemsExtras) do ids[#ids+1] = id end
|
|
return ids, string.format("%d-%d+extras", AE.ItemsDefaultFrom, AE.ItemsDefaultTo)
|
|
end
|
|
if arg == "missing" then
|
|
local ids = {}
|
|
for _, id in ipairs(AE.ItemsMissing) do ids[#ids+1] = id end
|
|
return ids, "missing"
|
|
end
|
|
local from, to = arg:match("^(%d+)%s+(%d+)$")
|
|
if from then
|
|
from, to = tonumber(from), tonumber(to)
|
|
if to < from then from, to = to, from end
|
|
-- Guard against a runaway range.
|
|
if (to - from) > 20000 then to = from + 20000 end
|
|
local ids = {}
|
|
for id = from, to do ids[#ids+1] = id end
|
|
return ids, string.format("%d-%d", from, to)
|
|
end
|
|
local single = arg:match("^(%d+)$")
|
|
if single then
|
|
return { tonumber(single) }, single
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
function AE.ItemsExport()
|
|
local out = {
|
|
schemaVersion = 1,
|
|
exportedAt = date("!%Y-%m-%dT%H:%M:%SZ"),
|
|
cacheMeta = CoaExporterItemCache.meta,
|
|
entries = {},
|
|
}
|
|
for _, rec in pairs(CoaExporterItemCache.entries) do
|
|
table.insert(out.entries, rec)
|
|
end
|
|
return out
|
|
end
|
|
|
|
function AE.ItemsReset()
|
|
CoaExporterItemCache = {
|
|
entries = {},
|
|
meta = { lastScanAt = nil, lastRange = nil, resolved = 0, unresolved = {} },
|
|
}
|
|
end
|
|
|
|
function AE.ItemsCount()
|
|
local n = 0
|
|
for _ in pairs(CoaExporterItemCache.entries) do n = n + 1 end
|
|
return n
|
|
end
|
|
|
|
AE._loadedItems = true
|