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.
This commit is contained in:
parent
7e5c58d1ca
commit
d0999ebda3
3 changed files with 288 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
231
CoaExporter/Collectors/Items.lua
Normal file
231
CoaExporter/Collectors/Items.lua
Normal 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
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue