Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 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