-- CoaExporter - Core local ADDON_NAME = ... CoaExporter = CoaExporter or {} local AE = CoaExporter CoaExporterConfig = CoaExporterConfig or {} CoaExporterConfig.enableSavedVariables = CoaExporterConfig.enableSavedVariables == true and true or false CoaExporterSaved = CoaExporterSaved or {} local function Norm(s) if s == nil then return nil end s = tostring(s):lower() s = s:match("^%s*(.-)%s*$") or s if s == "" then return nil end return s end local function SafeCall(fn, ...) local ok, r = pcall(fn, ...) if ok then return r end return nil end -- ===== Per-character export ===== function AE:AssembleExport(which) which = Norm(which) local nowIso = date("!%Y-%m-%dT%H:%M:%SZ") local version, build, buildDate, toc = GetBuildInfo() local name = UnitName("player") or "" local level = UnitLevel("player") or 0 local _, class = UnitClass("player") local _, race = UnitRace("player") local faction = UnitFactionGroup("player") or "" local realm = GetRealmName and GetRealmName() or (GetCVar and GetCVar("realmName")) or "" local out = { schemaVersion = 1, exportedAt = nowIso, client = { interface = toc or 30300, version = version, build = build, buildDate = buildDate }, character = { name = name or "", realm = realm or "", level = level or 0, class = class or "", race = race or "", faction = faction or "" }, } local wantAll = (which == nil) or (which == "all") if wantAll or which == "talents" then if AE.CollectTalents then out.talents = SafeCall(AE.CollectTalents) or {} end end if wantAll or which == "gear" then if AE.CollectGear then out.gear = SafeCall(AE.CollectGear) or {} end end if wantAll or which == "enchants" then if AE.CollectMysticEnchants then out.mysticEnchants = SafeCall(AE.CollectMysticEnchants) or {} end end return out end function AE:ShowExport(text, titleText) local show = _G.CoaExporter_ShowExportFrame if type(show) == "function" then show(text, titleText) return end if not self._fallbackFrame then local f = CreateFrame("Frame", "CoaExporterFallbackFrame", UIParent, "DialogBoxFrame") f:SetSize(700, 500) f:SetPoint("CENTER") f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", f.StartMoving) f:SetScript("OnDragStop", f.StopMovingOrSizing) local title = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") title:SetPoint("TOP", 0, -8) f.title = title local scroll = CreateFrame("ScrollFrame", "CoaExporterFallbackScroll", f, "UIPanelScrollFrameTemplate") scroll:SetPoint("TOPLEFT", 16, -36) scroll:SetPoint("BOTTOMRIGHT", -32, 16) local edit = CreateFrame("EditBox", "CoaExporterFallbackEdit", scroll) edit:SetMultiLine(true) edit:SetAutoFocus(true) edit:SetFontObject(ChatFontNormal) edit:SetWidth(640) edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) edit:SetScript("OnEditFocusGained", function(self) self:HighlightText() end) scroll:SetScrollChild(edit) f.editBox = edit f.scrollFrame = scroll local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") close:SetPoint("TOPRIGHT", -4, -4) self._fallbackFrame = f if type(_G.CoaExporter_ShowExportFrame) ~= "function" then _G.CoaExporter_ShowExportFrame = function(t, ti) local ff = AE._fallbackFrame if not ff then return end ff:Show() ff.editBox:SetText(t or "") ff.title:SetText(ti or "CoaExporter (Ctrl+C)") ff.editBox:HighlightText() ff.editBox:SetFocus() end end DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: using fallback export window (UI module not loaded).") end local f = self._fallbackFrame f:Show() f.editBox:SetText(text or "") f.title:SetText(titleText or "CoaExporter (Ctrl+C)") f.editBox:HighlightText() f.editBox:SetFocus() 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 json = encoder(data) local title = "CoaExporter" if normWhich and normWhich ~= "all" then title = title .. " - " .. normWhich end self:ShowExport(json, title .. " (Ctrl+C)") if CoaExporterConfig.enableSavedVariables then local key = (data.character.realm or "") .. ":" .. (data.character.name or "") CoaExporterSaved[key] = data DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: export saved to SavedVariables for " .. key) end end -- ===== Markdown generators ===== function AE:GenerateMarkdownGear() local gear = self.CollectGear and self.CollectGear() or { slots = {} } local ench = self.CollectMysticEnchants and self.CollectMysticEnchants() or { perSlot = {}, active = {} } local bySlot = {} for _, s in ipairs(gear.slots or {}) do bySlot[s.slot] = s end local mystPer = {} for _, e in ipairs(ench.perSlot or {}) do mystPer[e.slot] = e.name end local ORDER = { {1,"Head"},{2,"Neck"},{3,"Shoulders"},{15,"Back"},{5,"Chest"}, {9,"Wrist"},{16,"Mainhand"},{17,"Offhand"},{18,"Wand"},{10,"Gloves"}, {6,"Belt"},{7,"Legs"},{8,"Boots"},{11,"Ring 1"},{12,"Ring 2"}, {13,"Trinket 1"},{14,"Trinket 2"}, } local function md_link(name, itemId) if (itemId or 0) > 0 and name and name ~= "" then return string.format("[%s](https://db.ascension.gg/?item=%d)", name, itemId) elseif name and name ~= "" then return name else return "-" end end local lines = {} table.insert(lines, "| Slot | Item | Location | Enchant | Alternative |") table.insert(lines, "|------|------|----------|---------|-------------|") for _, ent in ipairs(ORDER) do local slotId, label = ent[1], ent[2] local s = bySlot[slotId] local itemCell, enchantCell = "-", "-" if s then itemCell = md_link(s.name, s.itemId) local gearEnch = s.enchantName if gearEnch and gearEnch ~= "" then enchantCell = gearEnch else local myst = mystPer[slotId] if myst and myst ~= "" then enchantCell = "Mystic: " .. myst end end end table.insert(lines, string.format("| **%s** | %s | - | %s | - |", label, itemCell, enchantCell)) end return table.concat(lines, "\n") .. "\n" end function AE:GenerateMarkdownEnchants() local ench = self.CollectMysticEnchants and self.CollectMysticEnchants() or { enchants = {} } local lines = {} table.insert(lines, "## Mystic Enchants\n") if #(ench.enchants or {}) == 0 then table.insert(lines, "*No Mystic Enchants equipped.*\n") return table.concat(lines, "\n") end local enchantRows = {} for _, e in ipairs(ench.enchants or {}) do local slot = e.slot or 0 local quality, qualityOrder if slot >= 11 then quality = 'Artifact'; qualityOrder = 1 elseif slot == 1 then quality = 'Legendary'; qualityOrder = 2 elseif slot >= 2 and slot <= 4 then quality = 'Epic'; qualityOrder = 3 else quality = 'Rare'; qualityOrder = 4 end local spellID = e.spellID or 0 local name = e.name or "Unknown" local icon = e.icon or "" local iconImg = icon ~= "" and string.format(' ', icon) or "" local link = spellID > 0 and string.format("[%s](https://db.ascension.gg/?spell=%d)", name, spellID) or name table.insert(enchantRows, { quality = quality, qualityOrder = qualityOrder, slot = slot, text = string.format("| %s | %s**%s** |", quality, iconImg, link), }) end table.sort(enchantRows, function(a, b) if a.qualityOrder ~= b.qualityOrder then return a.qualityOrder < b.qualityOrder end return a.slot < b.slot end) table.insert(lines, "| Quality | Enchant |") table.insert(lines, "|---------|---------|") for _, row in ipairs(enchantRows) do table.insert(lines, row.text) end return table.concat(lines, "\n") end function AE:GenerateMarkdownFull() local data = self:AssembleExport("all") or {} local char = data.character or {} local cls = (char.class or ""):lower() local classIcon = string.format("/icons/classes/classicon_%s.png", cls ~= "" and cls or "unknown") local lines = {} table.insert(lines, string.format("![%s](%s)", char.class or "Class", classIcon)) table.insert(lines, string.format("By %s", char.name or "?")) table.insert(lines, "") table.insert(lines, string.format("Character: **%s** - Level %d %s %s on %s", char.name or "?", char.level or 0, char.race or "?", char.class or "?", char.realm or "?")) table.insert(lines, "") table.insert(lines, "") table.insert(lines, "") table.insert(lines, "> **Note:** Exported by CoaExporter - gear, enchants, and talents are auto-populated.") table.insert(lines, "> Items marked with unknown db.ascension.gg IDs are custom Ascension loot.") table.insert(lines, "> {.is-info}") table.insert(lines, "") table.insert(lines, "## Gear") table.insert(lines, "") table.insert(lines, self:GenerateMarkdownGear()) table.insert(lines, self:GenerateMarkdownEnchants()) table.insert(lines, "") table.insert(lines, "## Talents") table.insert(lines, "") local talents = data.talents or {} local selected = talents.selected or {} local buildString = talents.buildString local list = talents.talents or selected if buildString and buildString ~= "" then table.insert(lines, string.format("**Ascension ExportBuild:** `%s`", buildString)) table.insert(lines, "") table.insert(lines, string.format( "") table.insert(lines, "") end if list and #list > 0 then table.insert(lines, string.format("Known talents: **%d** entries.", #list)) table.insert(lines, "") table.insert(lines, "| ID | Name | Rank |") table.insert(lines, "|----|------|------|") for _, t in ipairs(list) do local nm = (t.name and t.name ~= "") and t.name or "*(unresolved)*" local rk = (t.rank or 0) > 0 and tostring(t.rank) or "?" local mr = (t.maxRank or 0) > 0 and tostring(t.maxRank) or "?" table.insert(lines, string.format("| %d | %s | %s/%s |", t.id or 0, nm, rk, mr)) end else table.insert(lines, "*No talent data - run `/coae debug` to check collector.*") end return table.concat(lines, "\n") end -- ===== Slash command ===== local HELP = [[ CoaExporter commands: /coae export all|talents|gear|enchants /coae export mdgear|mdenchants|md (full wiki) /coae catalog all|skills|talents /coae catalog dispels [class]|passives [class]|status /coae scrolls scan|export|reset|status /coae sv on|off (SavedVariables for character export) /coae debug /coae help ]] local function HandleExport(rest) rest = Norm(rest) or "all" if rest == "mdgear" then AE:ShowExport(AE:GenerateMarkdownGear(), "CoaExporter - Markdown Gear (Ctrl+C)") elseif rest == "mdenchants" then AE:ShowExport(AE:GenerateMarkdownEnchants(), "CoaExporter - Markdown Enchants (Ctrl+C)") elseif rest == "md" or rest == "mdfull" or rest == "wiki" then AE:ShowExport(AE:GenerateMarkdownFull(), "CoaExporter - Wiki Markdown (Ctrl+C)") elseif rest == "all" or rest == "talents" or rest == "gear" or rest == "enchants" then AE:Export(rest) else DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown export target '" .. tostring(rest) .. "'") end end local function HandleCatalog(rest) rest = Norm(rest) or "status" local sub, arg = rest:match("^(%S+)%s*(.*)$") sub = sub or rest if sub == "all" or sub == "skills" or sub == "talents" then if not AE.Catalog or not AE.Catalog.Run then DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: catalog module not loaded") return end AE.Catalog.Run(sub == "all" and "all" or sub) elseif sub == "dispels" then if AE.CatalogListDispels then AE.CatalogListDispels(arg) end elseif sub == "passives" then if AE.CatalogListPassives then AE.CatalogListPassives(arg) end elseif sub == "status" then local meta = (CoaExporterCatalog and CoaExporterCatalog._meta) or {} local nSkills, nDispels, nPassives, nTalents = 0, 0, 0, 0 if CoaExporterCatalog then for _ in pairs(CoaExporterCatalog.skills or {}) do nSkills = nSkills + 1 end for _ in pairs(CoaExporterCatalog.dispels or {}) do nDispels = nDispels + 1 end for _ in pairs(CoaExporterCatalog.levelPassives or {}) do nPassives = nPassives + 1 end for _ in pairs(CoaExporterCatalog.talents or {}) do nTalents = nTalents + 1 end end DEFAULT_CHAT_FRAME:AddMessage(string.format( "CoaExporter catalog: lastScan=%s filter=%s | skills=%d cls dispels=%d cls passives=%d cls talents=%d cls", tostring(meta.lastScanAt or "never"), tostring(meta.filter or "-"), nSkills, nDispels, nPassives, nTalents)) else DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae catalog [all|skills|talents|dispels|passives|status]") end end local function HandleScrolls(rest) rest = Norm(rest) or "status" if rest == "scan" then if not AE.ScrollsStartScan then DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: MysticScrolls collector not loaded") return end DEFAULT_CHAT_FRAME:AddMessage(string.format( "CoaExporter scrolls: scanning %d scrolls, ~20s...", AE.ScrollCatalogTotal or 0)) AE.ScrollsStartScan(function(stats) DEFAULT_CHAT_FRAME:AddMessage(string.format( "CoaExporter scrolls: scan complete - %d resolved, %d unresolved", stats.total - stats.unresolved, stats.unresolved)) end) elseif rest == "export" then if not AE.ScrollsExport then DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: MysticScrolls collector not loaded") return end local data = AE.ScrollsExport() local encoder = _G.CoaExporter_Json_Encode if not encoder then DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: JSON encoder missing") return end AE:ShowExport(encoder(data), "CoaExporter - Scrolls (Ctrl+C)") elseif rest == "reset" then if AE.ScrollsReset then AE.ScrollsReset() end DEFAULT_CHAT_FRAME:AddMessage("CoaExporter scrolls: cache cleared") elseif rest == "status" then local meta = CoaExporterScrollCache and CoaExporterScrollCache.meta or {} local resolved = 0 if CoaExporterScrollCache then for _ in pairs(CoaExporterScrollCache.entries or {}) do resolved = resolved + 1 end end DEFAULT_CHAT_FRAME:AddMessage(string.format( "CoaExporter scrolls: %d/%d resolved (last scan: %s)", resolved, AE.ScrollCatalogTotal or 0, meta.lastScanAt and date("%Y-%m-%d %H:%M:%S", meta.lastScanAt) or "never")) else DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae scrolls [scan|export|reset|status]") end end local function HandleDebug() local lines = {} local function add(m) table.insert(lines, m) end add("CoaExporter debug:") 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 gear=%s enchants=%s scrolls=%s probe=%s catCommon=%s catSkills=%s catTalents=%s", tostring(AE._loadedTalents or false), tostring(AE._loadedGear or false), tostring(AE._loadedEnchants or false), tostring(AE._loadedMysticScrolls or false), tostring(AE._loadedMysticScrollProbe or false), tostring(AE._loadedCatalogCommon or false), tostring(AE._loadedCatalogSkills or false), tostring(AE._loadedCatalogTalents or false))) if type(AE.CollectTalents) == "function" then local t = SafeCall(AE.CollectTalents) or {} add(string.format("Talents: OK, selected=%d, build=%s", #(t.selected or {}), tostring(t.buildString or "-"))) else add("Talents: MISSING") end if type(AE.CollectGear) == "function" then local g = SafeCall(AE.CollectGear) or {} add(string.format("Gear: OK, slots=%d", #(g.slots or {}))) else add("Gear: MISSING") end if type(AE.CollectMysticEnchants) == "function" then local e = SafeCall(AE.CollectMysticEnchants) or {} add(string.format("MysticEnchants: OK, count=%d", #(e.enchants or {}))) else add("MysticEnchants: MISSING") end add(string.format("ScrollCatalog: %d entries", AE.ScrollCatalogTotal or 0)) local meta = (CoaExporterCatalog and CoaExporterCatalog._meta) or {} add(string.format("Catalog: lastScan=%s filter=%s", tostring(meta.lastScanAt or "never"), tostring(meta.filter or "-"))) AE:ShowExport(table.concat(lines, "\n"), "CoaExporter - Debug") end local function HandleSv(rest) if rest == "on" then CoaExporterConfig.enableSavedVariables = true DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: SavedVariables export ENABLED") elseif rest == "off" then CoaExporterConfig.enableSavedVariables = false DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: SavedVariables export DISABLED") else DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae sv on|off") end end local function Dispatch(msg) msg = (msg or ""):lower() if msg == "" or msg == "help" then AE:ShowExport(HELP, "CoaExporter - Help") return end local cmd, rest = msg:match("^(%S+)%s*(.*)$") 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 == "debug" then return HandleDebug() end if cmd == "sv" then return HandleSv(Norm(rest)) end DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown command. Type /coae help") end SLASH_COAE1 = "/coae" SLASH_COAE2 = "/coaexp" SLASH_COAE3 = "/ascx" -- back-compat: AscensionExporter users SLASH_COAE4 = "/asxc" SlashCmdList["COAE"] = Dispatch -- Legacy CoA_*Exporter slash compatibility shims SLASH_COAESKILLDUMP1 = "/skilldump" SlashCmdList["COAESKILLDUMP"] = function() if AE.Catalog and AE.Catalog.Run then AE.Catalog.Run("skills") end end SLASH_COAEDISPELS1 = "/dispels" SlashCmdList["COAEDISPELS"] = function(arg) if AE.CatalogListDispels then AE.CatalogListDispels(arg) end end SLASH_COAEPASSIVES1 = "/passives" SlashCmdList["COAEPASSIVES"] = function(arg) if AE.CatalogListPassives then AE.CatalogListPassives(arg) end end SLASH_COAETALENTDUMP1 = "/talentdumpall" SLASH_COAETALENTDUMP2 = "/talentdump" SlashCmdList["COAETALENTDUMP"] = function() if AE.Catalog and AE.Catalog.Run then AE.Catalog.Run("talents") end end local f = CreateFrame("Frame") f:RegisterEvent("ADDON_LOADED") f:SetScript("OnEvent", function(_, event, addon) if event == "ADDON_LOADED" and addon == "CoaExporter" then CoaExporterConfig.enableSavedVariables = CoaExporterConfig.enableSavedVariables and true or false end end)