Compare commits

..

1 commit

Author SHA1 Message Date
d0999ebda3 Add /coae items scanner (force item-template fetch + capture)
New Collectors/Items.lua mirrors the MysticScrolls async scanner: calls
GetItemInfo()/SetHyperlink() over an id list/range so the client fetches
the templates from the server. This both populates itemcache.wdb (for the
server-side re-import) and captures name/quality/ilvl/slot/icon/spell/
tooltip into the CoaExporterItemCache SavedVariable for direct export.

Commands:
  /coae items scan            scan the CoA custom block (2000000-2000099 + 2089618)
  /coae items scan missing    scan the known-missing id list
  /coae items scan <from> <to>  scan an explicit inclusive range
  /coae items scan <id>       single id
  /coae items export|reset|status

Motivation: CoA per-class character-creation gear (ids 2000000+) is only
cached once a character has viewed it, so unseen items are absent from
both the client cache and db.exil.es. This scans them in without needing
to roll one character per class.
2026-06-06 21:18:55 +02:00
3 changed files with 288 additions and 2 deletions

View file

@ -3,7 +3,7 @@
## Notes: Per-character export (talents/gear/mystic enchants/scrolls) + game-data catalog dump (skills/dispels/passives/talents) for db.exil.es
## Author: Subd from CoA / Exiles EU
## Version: 1.0.0
## SavedVariables: CoaExporterSaved, CoaExporterConfig, CoaExporterScrollCache, CoaExporterCatalog
## SavedVariables: CoaExporterSaved, CoaExporterConfig, CoaExporterScrollCache, CoaExporterCatalog, CoaExporterItemCache
Util\Json.lua
Data\ScrollCatalog.lua
@ -14,6 +14,7 @@ Collectors\Gear.lua
Collectors\Enchants.lua
Collectors\MysticScrolls.lua
Collectors\MysticScrollProbe.lua
Collectors\Items.lua
Catalogs\Common.lua
Catalogs\Skills.lua

View file

@ -0,0 +1,231 @@
-- 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; this is also what writes
-- the row into itemcache.wdb.
GameTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
GameTooltip:SetHyperlink("item:" .. itemID)
GameTooltip:Hide()
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.
function AE.ItemsScanPass(ids)
local total = #ids
local newlyResolved = 0
local unresolved = {}
for _, id in ipairs(ids) do
if TryResolve(id) then
newlyResolved = newlyResolved + 1
else
unresolved[#unresolved+1] = id
end
end
CoaExporterItemCache.meta.lastScanAt = time()
CoaExporterItemCache.meta.resolved = newlyResolved
CoaExporterItemCache.meta.unresolved = unresolved
return { total = total, resolved = 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 this pass %d/%d (still unresolved: %d)",
attempts, maxAttempts, stats.resolved, stats.total, 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

View file

@ -381,6 +381,8 @@ CoaExporter commands:
/coae catalog all|skills|talents|icons
/coae catalog dispels [class]|passives [class]|status
/coae scrolls scan|export|reset|status
/coae items scan [missing | <from> <to> | <id>]
/coae items export|reset|status
/coae sv on|off (SavedVariables for character export)
/coae debug
/coae help
@ -481,6 +483,55 @@ local function HandleScrolls(rest)
end
end
local function HandleItems(rest)
rest = rest or ""
local sub, arg = rest:match("^(%S*)%s*(.*)$")
sub = (sub or ""):lower()
if sub == "" or sub == "scan" then
if not AE.ItemsStartScan then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: items collector not loaded")
return
end
local ids, label = AE.ItemsResolveTargets(arg)
if not ids then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae items scan [missing | <from> <to> | <id>]")
return
end
CoaExporterItemCache.meta.lastRange = label
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter items: scanning %d id(s) [%s], up to ~25s...", #ids, label))
AE.ItemsStartScan(ids, function(stats)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter items: scan complete - %d cached total, %d unresolved this run. "
.. "Log out to flush itemcache.wdb, then '/coae items export'.",
AE.ItemsCount and AE.ItemsCount() or 0, stats.unresolved))
end)
elseif sub == "export" then
if not AE.ItemsExport then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: items collector not loaded")
return
end
local encoder = _G.CoaExporter_Json_Encode
if not encoder then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: JSON encoder missing")
return
end
AE:ShowExport(encoder(AE.ItemsExport()), "CoaExporter - Items (Ctrl+C)")
elseif sub == "reset" then
if AE.ItemsReset then AE.ItemsReset() end
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter items: cache cleared")
elseif sub == "status" then
local meta = CoaExporterItemCache and CoaExporterItemCache.meta or {}
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter items: %d cached (last range: %s, last scan: %s)",
AE.ItemsCount and AE.ItemsCount() or 0,
tostring(meta.lastRange or "-"),
meta.lastScanAt and date("%Y-%m-%d %H:%M:%S", meta.lastScanAt) or "never"))
else
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae items [scan|export|reset|status]")
end
end
local function HandleDebug()
local lines = {}
local function add(m) table.insert(lines, m) end
@ -489,10 +540,11 @@ local function HandleDebug()
add(string.format("- AddOn: %s", tostring(ADDON_NAME)))
add(string.format("- UI: %s", type(_G.CoaExporter_ShowExportFrame) == "function" and "yes" or "no"))
add(string.format("- JSON: %s", type(_G.CoaExporter_Json_Encode) == "function" and "yes" or "no"))
add(string.format("- Loaded: talents=%s spellbook=%s gear=%s enchants=%s scrolls=%s probe=%s catCommon=%s catSkills=%s catTalents=%s",
add(string.format("- Loaded: talents=%s spellbook=%s gear=%s enchants=%s scrolls=%s probe=%s items=%s catCommon=%s catSkills=%s catTalents=%s",
tostring(AE._loadedTalents or false), tostring(AE._loadedSpellbook or false),
tostring(AE._loadedGear or false), tostring(AE._loadedEnchants or false),
tostring(AE._loadedMysticScrolls or false), tostring(AE._loadedMysticScrollProbe or false),
tostring(AE._loadedItems or false),
tostring(AE._loadedCatalogCommon or false), tostring(AE._loadedCatalogSkills or false),
tostring(AE._loadedCatalogTalents or false)))
@ -523,6 +575,7 @@ local function HandleDebug()
add("MysticEnchants: MISSING")
end
add(string.format("ScrollCatalog: %d entries", AE.ScrollCatalogTotal or 0))
add(string.format("Items: %d cached", AE.ItemsCount and AE.ItemsCount() or 0))
local meta = (CoaExporterCatalog and CoaExporterCatalog._meta) or {}
add(string.format("Catalog: lastScan=%s filter=%s",
@ -553,6 +606,7 @@ local function Dispatch(msg)
if cmd == "export" then return HandleExport(rest) end
if cmd == "catalog" then return HandleCatalog(rest) end
if cmd == "scrolls" then return HandleScrolls(rest) end
if cmd == "items" then return HandleItems(rest) end
if cmd == "debug" then return HandleDebug() end
if cmd == "sv" then return HandleSv(Norm(rest)) end
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown command. Type /coae help")