-- AscensionExporter - Core local ADDON_NAME = ... AscensionExporter = AscensionExporter or {} local AE = AscensionExporter -- SavedVariables defaults AscensionExporterConfig = AscensionExporterConfig or {} AscensionExporterConfig.enableSavedVariables = AscensionExporterConfig.enableSavedVariables == true and true or false AscensionExporterSaved = AscensionExporterSaved or {} -- Utils local function IsArray(t) if type(t) ~= "table" then return false end local n = 0 for k, _ in pairs(t) do if type(k) ~= "number" then return false end n = n + 1 end for i = 1, n do if t[i] == nil then return false end end return true end -- normalize simple string args (trim + lower) 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 -- Shims for safety local function SafeCall(fn, ...) local ok, r = pcall(fn, ...) if ok then return r end return nil end -- Export assembly 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 -- Unified show function with safe fallback export window function AE:ShowExport(text, titleText) local show = _G.AscensionExporter_ShowExportFrame if type(show) == "function" then show(text, titleText) return end -- Fallback lightweight frame (created once) if not self._fallbackFrame then local f = CreateFrame("Frame", "AscensionExporterFallbackFrame", 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", "AscensionExporterFallbackScroll", f, "UIPanelScrollFrameTemplate") scroll:SetPoint("TOPLEFT", 16, -36) scroll:SetPoint("BOTTOMRIGHT", -32, 16) local edit = CreateFrame("EditBox", "AscensionExporterFallbackEdit", 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 -- Provide a global for other callers until UI module is available. -- IMPORTANT: Do NOT route this shim back into AE:ShowExport(), -- to avoid infinite recursion. Directly update the fallback frame. if type(_G.AscensionExporter_ShowExportFrame) ~= "function" then _G.AscensionExporter_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 "Ascension Export - Copy All (Ctrl+C)") ff.editBox:HighlightText() ff.editBox:SetFocus() end end DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: 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 "Ascension Export - Copy All (Ctrl+C)") f.editBox:HighlightText() f.editBox:SetFocus() end function AE:Export(which) local normWhich = Norm(which) local data = self:AssembleExport(normWhich) -- Prefer global encoder from Util/Json.lua; fallback to a tiny local encoder if missing 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 -- detect array-like 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.AscensionExporter_Json_Encode or tiny_json_encode local json = encoder(data) local title = "Ascension Export" if normWhich and normWhich ~= "all" then title = title .. " - " .. normWhich end self:ShowExport(json, title .. " - Copy All (Ctrl+C)") if AscensionExporterConfig.enableSavedVariables then local key = (data.character.realm or "") .. ":" .. (data.character.name or "") AscensionExporterSaved[key] = data DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: export saved to SavedVariables for " .. key) end end -- Markdown gear table generator (for guide tables like mage-guide.md) function AE:GenerateMarkdownGear() local gear = self.CollectGear and self.CollectGear() or { slots = {} } local ench = self.CollectMysticEnchants and self.CollectMysticEnchants() or { perSlot = {}, active = {} } -- Map gear by slot and mystic enchant per slot 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 -- Desired order and pretty slot names matching the mage guide 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 = "-" local enchantCell = "-" if s then itemCell = md_link(s.name, s.itemId) local base = s.enchant and s.enchant.name or nil local myst = mystPer[slotId] if base and base ~= "" then enchantCell = base end if myst and myst ~= "" then enchantCell = (enchantCell ~= "-" and enchantCell ~= nil and enchantCell ~= "") and (enchantCell .. " / Mystic: " .. myst) or ("Mystic: " .. myst) end if not enchantCell or enchantCell == "" then enchantCell = "-" end end local row = string.format("| **%s** | %s | - | %s | - |", label, itemCell, enchantCell) table.insert(lines, row) end return table.concat(lines, "\n") .. "\n" end -- Slash command handling SLASH_ASCX1 = "/ascx" SLASH_ASCX2 = "/asxc" -- alias: both map to the same handler key "ASCX" SlashCmdList["ASCX"] = function(msg) msg = msg or "" msg = msg:lower() if msg == "" or msg == "help" then -- Open the export window with helpful text. If the UI frame isn't available, -- avoid referencing buttons that won't be present in the fallback window. local hasButtons = type(_G.AscensionExporter_ShowExportFrame) == "function" local prefix = hasButtons and "Use the buttons above or type:" or "Type one of the commands:" local help = prefix .. "\n/ascx export all|talents|gear|enchants|mdgear\n/ascx sv on|off (SavedVariables is currently " .. (AscensionExporterConfig.enableSavedVariables and "ON" or "OFF") .. ")\n/ascx debug (show collector status)\n" AE:ShowExport(help, "Ascension Export - Tools & Help") return end local cmd, rest = msg:match("^(%S+)%s*(.*)$") if cmd == "export" then rest = Norm(rest) or "all" if rest == "mdgear" or rest == "md" then local md = AE:GenerateMarkdownGear() AE:ShowExport(md, "Ascension Export - Markdown Gear - Copy All (Ctrl+C)") elseif rest == "all" or rest == "talents" or rest == "gear" or rest == "enchants" then AE:Export(rest) else DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: unknown export target '" .. tostring(rest) .. "'") end return elseif cmd == "debug" then local lines = {} local function add(msg) table.insert(lines, msg) end add("AscensionExporter debug:") add(string.format("- AddOn: %s", tostring(ADDON_NAME))) add(string.format("- UI available: %s", type(_G.AscensionExporter_ShowExportFrame) == "function" and "yes" or "no")) add(string.format("- JSON encoder: %s", type(_G.AscensionExporter_Json_Encode) == "function" and "yes" or "no")) add(string.format("- Modules loaded flags: talents=%s gear=%s enchants=%s", tostring(AE._loadedTalents or false), tostring(AE._loadedGear or false), tostring(AE._loadedEnchants or false))) -- Talents if type(AE.CollectTalents) == "function" then local t = SafeCall(AE.CollectTalents) or {} local n = #(t.selected or {}) add(string.format("Talents: OK, selected=%d", n)) else add("Talents: MISSING function") end -- Gear if type(AE.CollectGear) == "function" then local g = SafeCall(AE.CollectGear) or {} local n = #(g.slots or {}) add(string.format("Gear: OK, slots=%d", n)) else add("Gear: MISSING function") end -- Enchants if type(AE.CollectMysticEnchants) == "function" then local e = SafeCall(AE.CollectMysticEnchants) or {} local nps = #(e.perSlot or {}) local na = #(e.active or {}) add(string.format("MysticEnchants: OK, perSlot=%d, active=%d", nps, na)) else add("MysticEnchants: MISSING function") end AE:ShowExport(table.concat(lines, "\n"), "Ascension Export - Debug") return elseif cmd == "sv" then if rest == "on" then AscensionExporterConfig.enableSavedVariables = true DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: SavedVariables export ENABLED") elseif rest == "off" then AscensionExporterConfig.enableSavedVariables = false DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: SavedVariables export DISABLED") else DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: sv on|off") end return else DEFAULT_CHAT_FRAME:AddMessage("AscensionExporter: unknown command. Type /ascx help") end end -- Basic event binding local f = CreateFrame("Frame") f:RegisterEvent("ADDON_LOADED") f:SetScript("OnEvent", function(_, event, addon) if event == "ADDON_LOADED" and addon == "AscensionExporter" then AscensionExporterConfig.enableSavedVariables = AscensionExporterConfig.enableSavedVariables and true or false end end)