94f6f55fb6
- Introduced `install_addon_sub.sh` to automate addon installation. - Improved argument normalization with a new `Norm` helper function. - Added debug command for enhanced diagnostics and collector status visibility. - Ensured global addon table initialization for flexible load order. - Marked modules as loaded for better debug tracking. - Fixed TOC path formatting for Windows compatibility. - Updated `.gitignore` rules for IDE folders.
365 lines
14 KiB
Lua
365 lines
14 KiB
Lua
-- 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)
|