feat: add Database options tab with import/export/cleanup
New files:
- Modules/QuestieLearnerExport.lua: serialize/compress/encode pipeline using
AceSerializer + LibDeflate:CompressDeflate(level=9) + EncodeForPrint.
QxLD:<version>!<encoded> string format. Export(), ExportAll(), ValidateImport(),
MergeImport(), DryRunPrune(), Prune().
- Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua: new Database tab with:
- Live learned data stats (per server)
- Per-type learner toggles (NPCs/Quests/Objects/Items/Broadcast)
- Export current server / Export all servers buttons (opens scrollable dialog)
- Import window (paste -> Validate -> Import with diff summary)
- Dry-run preview and Prune Now cleanup buttons
- Full reset button (confirm-gated)
- Contribute section with step-by-step GitHub submission instructions
Wiring:
- QuestieOptions.lua: database_tab initialized and registered in args
- All TOC files: QuestieLearnerExport.lua added before options, DatabaseTab lua added
This commit is contained in:
@@ -0,0 +1,441 @@
|
|||||||
|
---@type QuestieOptions
|
||||||
|
local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions")
|
||||||
|
---@type l10n
|
||||||
|
local l10n = QuestieLoader:ImportModule("l10n")
|
||||||
|
|
||||||
|
QuestieOptions.tabs.database = {}
|
||||||
|
|
||||||
|
local AceGUI = LibStub("AceGUI-3.0")
|
||||||
|
|
||||||
|
-- Forward declarations for dialog functions
|
||||||
|
local _OpenExportDialog, _OpenImportDialog
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Dialog helpers
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function GetExportModule()
|
||||||
|
return QuestieLoader:ImportModule("QuestieLearnerExport")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function GetServer()
|
||||||
|
if Questie.IsAscension then return "Ascension" end
|
||||||
|
if Questie.IsTurtle then return "Turtle" end
|
||||||
|
if Questie.IsEbonhold then return "Ebonhold" end
|
||||||
|
if Questie.IsEra then return "Era" end
|
||||||
|
if Questie.Is335 then return "WotLK" end
|
||||||
|
return GetRealmName and GetRealmName() or "unknown"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function GetLearnedCounts()
|
||||||
|
local ld = Questie.db and Questie.db.global and Questie.db.global.learnedData
|
||||||
|
local none = { npcs = 0, quests = 0, items = 0, objects = 0, total = 0 }
|
||||||
|
if not ld then return none end
|
||||||
|
local bucket = ld[GetServer()] or (ld.npcs and ld) or nil
|
||||||
|
if not bucket then return none end
|
||||||
|
local function Count(t)
|
||||||
|
if not t then return 0 end
|
||||||
|
local n = 0; for _ in pairs(t) do n = n + 1 end; return n
|
||||||
|
end
|
||||||
|
local s = {
|
||||||
|
npcs = Count(bucket.npcs),
|
||||||
|
quests = Count(bucket.quests),
|
||||||
|
items = Count(bucket.items),
|
||||||
|
objects = Count(bucket.objects),
|
||||||
|
}
|
||||||
|
s.total = s.npcs + s.quests + s.items + s.objects
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Export Dialog
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
_OpenExportDialog = function(exportStr, stats)
|
||||||
|
local f = AceGUI:Create("Frame")
|
||||||
|
f:SetTitle("Questie-X — Export Learned Data")
|
||||||
|
f:SetWidth(620)
|
||||||
|
f:SetHeight(420)
|
||||||
|
f:SetLayout("Flow")
|
||||||
|
f:SetCallback("OnClose", function(w) AceGUI:Release(w) end)
|
||||||
|
|
||||||
|
local info = AceGUI:Create("Label")
|
||||||
|
info:SetFullWidth(true)
|
||||||
|
info:SetText(string.format(
|
||||||
|
"|cFFFFD700Server:|r %s |cFFFFD700NPCs:|r %d |cFFFFD700Quests:|r %d |cFFFFD700Items:|r %d |cFFFFD700Objects:|r %d |cFFFFD700Total:|r %d entries",
|
||||||
|
GetServer(), stats.npcs or 0, stats.quests or 0, stats.items or 0, stats.objects or 0, stats.total or 0
|
||||||
|
))
|
||||||
|
f:AddChild(info)
|
||||||
|
|
||||||
|
local spacer = AceGUI:Create("Label")
|
||||||
|
spacer:SetFullWidth(true)
|
||||||
|
spacer:SetText(" ")
|
||||||
|
f:AddChild(spacer)
|
||||||
|
|
||||||
|
local box = AceGUI:Create("MultiLineEditBox")
|
||||||
|
box:SetFullWidth(true)
|
||||||
|
box:SetNumLines(14)
|
||||||
|
box:SetLabel("Select all and copy (Ctrl+A, Ctrl+C):")
|
||||||
|
box:SetText(exportStr)
|
||||||
|
box:DisableButton(true)
|
||||||
|
f:AddChild(box)
|
||||||
|
|
||||||
|
local hint = AceGUI:Create("Label")
|
||||||
|
hint:SetFullWidth(true)
|
||||||
|
hint:SetText("|cFF888888To share your data: copy this string and submit it on the Questie-X GitHub or Discord. Contributors help grow the quest database for everyone.|r")
|
||||||
|
f:AddChild(hint)
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Import Dialog
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
_OpenImportDialog = function()
|
||||||
|
local f = AceGUI:Create("Frame")
|
||||||
|
f:SetTitle("Questie-X — Import Learned Data")
|
||||||
|
f:SetWidth(620)
|
||||||
|
f:SetHeight(420)
|
||||||
|
f:SetLayout("Flow")
|
||||||
|
f:SetCallback("OnClose", function(w) AceGUI:Release(w) end)
|
||||||
|
|
||||||
|
local statusLabel = AceGUI:Create("Label")
|
||||||
|
statusLabel:SetFullWidth(true)
|
||||||
|
statusLabel:SetText("|cFF888888Paste a QxLD export string below, then click Validate.|r")
|
||||||
|
f:AddChild(statusLabel)
|
||||||
|
|
||||||
|
local spacer = AceGUI:Create("Label")
|
||||||
|
spacer:SetFullWidth(true)
|
||||||
|
spacer:SetText(" ")
|
||||||
|
f:AddChild(spacer)
|
||||||
|
|
||||||
|
local box = AceGUI:Create("MultiLineEditBox")
|
||||||
|
box:SetFullWidth(true)
|
||||||
|
box:SetNumLines(12)
|
||||||
|
box:SetLabel("Paste export string here:")
|
||||||
|
box:SetText("")
|
||||||
|
box:DisableButton(true)
|
||||||
|
f:AddChild(box)
|
||||||
|
|
||||||
|
local validateBtn = AceGUI:Create("Button")
|
||||||
|
validateBtn:SetText("Validate")
|
||||||
|
validateBtn:SetWidth(120)
|
||||||
|
f:AddChild(validateBtn)
|
||||||
|
|
||||||
|
local importBtn = AceGUI:Create("Button")
|
||||||
|
importBtn:SetText("Import")
|
||||||
|
importBtn:SetWidth(120)
|
||||||
|
importBtn:SetDisabled(true)
|
||||||
|
f:AddChild(importBtn)
|
||||||
|
|
||||||
|
validateBtn:SetCallback("OnClick", function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then
|
||||||
|
statusLabel:SetText("|cFFFF0000Export module not loaded.|r")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local input = box:GetText()
|
||||||
|
local payload, statsOrErr = Exp:ValidateImport(input)
|
||||||
|
if not payload then
|
||||||
|
statusLabel:SetText("|cFFFF0000Validation failed: " .. tostring(statsOrErr) .. "|r")
|
||||||
|
importBtn:SetDisabled(true)
|
||||||
|
else
|
||||||
|
statusLabel:SetText(string.format(
|
||||||
|
"|cFF00FF00Valid! Server: %s NPCs: %d Quests: %d Items: %d Objects: %d Total: %d|r",
|
||||||
|
statsOrErr.server or "?",
|
||||||
|
statsOrErr.npcs or 0, statsOrErr.quests or 0,
|
||||||
|
statsOrErr.items or 0, statsOrErr.objects or 0,
|
||||||
|
statsOrErr.total or 0
|
||||||
|
))
|
||||||
|
importBtn:SetDisabled(false)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
importBtn:SetCallback("OnClick", function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then return end
|
||||||
|
local ok, msg = Exp:MergeImport()
|
||||||
|
if ok then
|
||||||
|
statusLabel:SetText("|cFF00FF00" .. msg .. "|r")
|
||||||
|
importBtn:SetDisabled(true)
|
||||||
|
else
|
||||||
|
statusLabel:SetText("|cFFFF0000" .. msg .. "|r")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Tab Definition
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function QuestieOptions.tabs.database:Initialize()
|
||||||
|
return {
|
||||||
|
name = function() return l10n("Database") end,
|
||||||
|
type = "group",
|
||||||
|
order = 9,
|
||||||
|
args = {
|
||||||
|
|
||||||
|
---- Header -----------------------------------------------
|
||||||
|
db_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 1,
|
||||||
|
name = function() return l10n("Learned Data") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Live stats description --------------------------------
|
||||||
|
db_stats_desc = {
|
||||||
|
type = "description",
|
||||||
|
order = 1.1,
|
||||||
|
fontSize = "medium",
|
||||||
|
name = function()
|
||||||
|
local s = GetLearnedCounts()
|
||||||
|
if s.total == 0 then
|
||||||
|
return "|cFF888888No learned data recorded yet. Play the game and Questie will learn as you go.|r"
|
||||||
|
end
|
||||||
|
return string.format(
|
||||||
|
"|cFFFFD700Server:|r %s\n|cFF5EBAF3NPCs:|r %d |cFF5EBAF3Quests:|r %d |cFF5EBAF3Items:|r %d |cFF5EBAF3Objects:|r %d\n|cFFFFFFFFTotal entries:|r %d",
|
||||||
|
GetServer(), s.npcs, s.quests, s.items, s.objects, s.total
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Learner Toggles header --------------------------------
|
||||||
|
learner_toggle_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 2,
|
||||||
|
name = function() return l10n("What To Learn") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
learn_npcs = {
|
||||||
|
type = "toggle",
|
||||||
|
order = 2.1,
|
||||||
|
name = function() return l10n("Learn NPCs") end,
|
||||||
|
desc = function() return l10n("Record quest-relevant NPC positions and data.") end,
|
||||||
|
get = function() return Questie.db.global.learnedData and Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.learnNpcs end,
|
||||||
|
set = function(_, v)
|
||||||
|
if Questie.db.global.learnedData and Questie.db.global.learnedData.settings then
|
||||||
|
Questie.db.global.learnedData.settings.learnNpcs = v
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
learn_quests = {
|
||||||
|
type = "toggle",
|
||||||
|
order = 2.2,
|
||||||
|
name = function() return l10n("Learn Quests") end,
|
||||||
|
desc = function() return l10n("Record quest metadata, objectives, and rewards.") end,
|
||||||
|
get = function() return Questie.db.global.learnedData and Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.learnQuests end,
|
||||||
|
set = function(_, v)
|
||||||
|
if Questie.db.global.learnedData and Questie.db.global.learnedData.settings then
|
||||||
|
Questie.db.global.learnedData.settings.learnQuests = v
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
learn_objects = {
|
||||||
|
type = "toggle",
|
||||||
|
order = 2.3,
|
||||||
|
name = function() return l10n("Learn Objects") end,
|
||||||
|
desc = function() return l10n("Record interactable quest object positions.") end,
|
||||||
|
get = function() return Questie.db.global.learnedData and Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.learnObjects end,
|
||||||
|
set = function(_, v)
|
||||||
|
if Questie.db.global.learnedData and Questie.db.global.learnedData.settings then
|
||||||
|
Questie.db.global.learnedData.settings.learnObjects = v
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
learn_items = {
|
||||||
|
type = "toggle",
|
||||||
|
order = 2.4,
|
||||||
|
name = function() return l10n("Learn Items") end,
|
||||||
|
desc = function() return l10n("Record quest item drop sources.") end,
|
||||||
|
get = function() return Questie.db.global.learnedData and Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.learnItems end,
|
||||||
|
set = function(_, v)
|
||||||
|
if Questie.db.global.learnedData and Questie.db.global.learnedData.settings then
|
||||||
|
Questie.db.global.learnedData.settings.learnItems = v
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
learn_broadcast = {
|
||||||
|
type = "toggle",
|
||||||
|
order = 2.5,
|
||||||
|
name = function() return l10n("Broadcast to Party") end,
|
||||||
|
desc = function() return l10n("Share newly learned data with nearby party/raid members who also have Questie-X.") end,
|
||||||
|
get = function() return Questie.db.profile.learnerBroadcast end,
|
||||||
|
set = function(_, v) Questie.db.profile.learnerBroadcast = v end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Export -----------------------------------------------
|
||||||
|
export_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 3,
|
||||||
|
name = function() return l10n("Export") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
export_desc = {
|
||||||
|
type = "description",
|
||||||
|
order = 3.1,
|
||||||
|
fontSize = "medium",
|
||||||
|
name = function()
|
||||||
|
return "|cFF888888Export your learned data as a compressed string. You can paste this in a GitHub issue or Discord message to help improve the official Questie databases.|r"
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
export_current_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 3.2,
|
||||||
|
name = function() return l10n("Export Current Server") end,
|
||||||
|
desc = function()
|
||||||
|
local s = GetLearnedCounts()
|
||||||
|
return string.format("Export %d entries for %s", s.total, GetServer())
|
||||||
|
end,
|
||||||
|
func = function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then
|
||||||
|
Questie:Print("|cFFFF0000QuestieLearnerExport module not loaded.|r")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local str, statsOrErr = Exp:Export()
|
||||||
|
if not str then
|
||||||
|
Questie:Print("|cFFFF0000Export failed: " .. tostring(statsOrErr) .. "|r")
|
||||||
|
else
|
||||||
|
_OpenExportDialog(str, statsOrErr)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
export_all_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 3.3,
|
||||||
|
name = function() return l10n("Export All Servers") end,
|
||||||
|
desc = function() return "Export merged data from all server profiles." end,
|
||||||
|
func = function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then
|
||||||
|
Questie:Print("|cFFFF0000QuestieLearnerExport module not loaded.|r")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local str, statsOrErr = Exp:ExportAll()
|
||||||
|
if not str then
|
||||||
|
Questie:Print("|cFFFF0000Export failed: " .. tostring(statsOrErr) .. "|r")
|
||||||
|
else
|
||||||
|
_OpenExportDialog(str, statsOrErr)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Import -----------------------------------------------
|
||||||
|
import_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 4,
|
||||||
|
name = function() return l10n("Import") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
import_desc = {
|
||||||
|
type = "description",
|
||||||
|
order = 4.1,
|
||||||
|
fontSize = "medium",
|
||||||
|
name = function()
|
||||||
|
return "|cFF888888Import a QxLD string from another player or the Questie-X GitHub. Questie-X will validate the string before merging — existing data with higher confidence is never overwritten.|r"
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
import_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 4.2,
|
||||||
|
name = function() return l10n("Open Import Window") end,
|
||||||
|
func = function()
|
||||||
|
_OpenImportDialog()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Cleanup ----------------------------------------------
|
||||||
|
cleanup_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 5,
|
||||||
|
name = function() return l10n("Cleanup") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup_desc = {
|
||||||
|
type = "description",
|
||||||
|
order = 5.1,
|
||||||
|
fontSize = "medium",
|
||||||
|
name = function()
|
||||||
|
return "|cFF888888Remove stale, empty, or low-confidence entries from your learned data. Run a dry-run first to see what would be removed.|r"
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
prune_dry_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 5.2,
|
||||||
|
name = function() return l10n("Dry Run (Preview)") end,
|
||||||
|
desc = function() return "Print a summary of entries that would be removed, without deleting anything." end,
|
||||||
|
func = function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then return end
|
||||||
|
local r = Exp:DryRunPrune()
|
||||||
|
Questie:Print(string.format(
|
||||||
|
"|cFF00FF00[Learner Prune Preview]|r Would remove: NPCs %d, Quests %d, Items %d, Objects %d — Total %d",
|
||||||
|
r.npcs, r.quests, r.items, r.objects, r.total
|
||||||
|
))
|
||||||
|
if r.total > 0 then
|
||||||
|
Questie:Print("|cFF888888Use /questie db prune to apply, or click Prune Now in the Database tab.|r")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
prune_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 5.3,
|
||||||
|
name = function() return l10n("Prune Now") end,
|
||||||
|
desc = function() return "|cFFFF8800Removes stale entries. Cannot be undone. Export first if you want a backup.|r" end,
|
||||||
|
func = function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then return end
|
||||||
|
local r = Exp:Prune()
|
||||||
|
Questie:Print(string.format(
|
||||||
|
"|cFF00FF00[Learner Prune]|r Removed: NPCs %d, Quests %d, Items %d, Objects %d — Total %d",
|
||||||
|
r.npcs, r.quests, r.items, r.objects, r.total
|
||||||
|
))
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
prune_all_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 5.4,
|
||||||
|
name = function() return "|cFFFF4444" .. l10n("Reset All Learned Data") .. "|r" end,
|
||||||
|
desc = function() return "|cFFFF0000DANGER: Wipes ALL learned data for ALL servers. Export first.|r" end,
|
||||||
|
confirm = true,
|
||||||
|
confirmText = "Are you sure? This cannot be undone.",
|
||||||
|
func = function()
|
||||||
|
if Questie.db and Questie.db.global then
|
||||||
|
Questie.db.global.learnedData = nil
|
||||||
|
Questie:Print("|cFFFF4444[Questie-X]|r All learned data has been reset.")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
---- Submit/Contribute ------------------------------------
|
||||||
|
submit_header = {
|
||||||
|
type = "header",
|
||||||
|
order = 6,
|
||||||
|
name = function() return l10n("Contribute") end,
|
||||||
|
},
|
||||||
|
|
||||||
|
submit_desc = {
|
||||||
|
type = "description",
|
||||||
|
order = 6.1,
|
||||||
|
fontSize = "medium",
|
||||||
|
name = function()
|
||||||
|
return "|cFF888888Want to help grow the Questie-X database?\n\n" ..
|
||||||
|
"1. Click |r|cFFFFFFFFExport Current Server|r|cFF888888 above.\n" ..
|
||||||
|
"2. Copy the full export string (Ctrl+A then Ctrl+C in the dialog).\n" ..
|
||||||
|
"3. Open a new GitHub issue at |r|cFF5EBAF3github.com/Xurkon/Questie-X/issues|r|cFF888888 titled: |r|cFFFFFFFF[Data Submission] <Server Name>|r|cFF888888\n" ..
|
||||||
|
"4. Paste the string into the issue body and submit.\n\n" ..
|
||||||
|
"Submissions are reviewed and merged into the official database. Thank you for contributing!|r"
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
@@ -137,6 +137,8 @@ _CreateOptionsTable = function()
|
|||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
local advanced_tab = QuestieOptions.tabs.advanced:Initialize()
|
local advanced_tab = QuestieOptions.tabs.advanced:Initialize()
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
|
local database_tab = QuestieOptions.tabs.database:Initialize()
|
||||||
|
coroutine.yield()
|
||||||
local credits_tab = QuestieOptions.tabs.credits:Initialize()
|
local credits_tab = QuestieOptions.tabs.credits:Initialize()
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
return {
|
return {
|
||||||
@@ -157,6 +159,7 @@ _CreateOptionsTable = function()
|
|||||||
nameplate_tab = nameplate_tab,
|
nameplate_tab = nameplate_tab,
|
||||||
dbm_hud_tab = dbm_hud_tab,
|
dbm_hud_tab = dbm_hud_tab,
|
||||||
advanced_tab = advanced_tab,
|
advanced_tab = advanced_tab,
|
||||||
|
database_tab = database_tab,
|
||||||
credits_tab = credits_tab,
|
credits_tab = credits_tab,
|
||||||
profiles_tab = LibStub("AceDBOptions-3.0"):GetOptionsTable(Questie.db)
|
profiles_tab = LibStub("AceDBOptions-3.0"):GetOptionsTable(Questie.db)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
---@class QuestieLearnerExport
|
||||||
|
local QuestieLearnerExport = QuestieLoader:CreateModule("QuestieLearnerExport")
|
||||||
|
|
||||||
|
---@type QuestieLearner
|
||||||
|
local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner")
|
||||||
|
---@type QuestieServer
|
||||||
|
local QuestieServer = QuestieLoader:ImportModule("QuestieServer")
|
||||||
|
|
||||||
|
local LibDeflate = LibStub("LibDeflate")
|
||||||
|
local AceSerializer = LibStub("AceSerializer-3.0")
|
||||||
|
|
||||||
|
local FORMAT_PREFIX = "QxLD"
|
||||||
|
local FORMAT_VERSION = 1
|
||||||
|
local FORMAT_SEP = "!"
|
||||||
|
local MAX_IMPORT_LEN = 524288 -- 512 KB hard cap on raw decoded payload
|
||||||
|
|
||||||
|
local _Export = QuestieLearnerExport.private or {}
|
||||||
|
QuestieLearnerExport.private = _Export
|
||||||
|
|
||||||
|
-- Cached last export string and stats for the UI to read without re-computing
|
||||||
|
QuestieLearnerExport.lastExportString = nil
|
||||||
|
QuestieLearnerExport.lastExportStats = nil
|
||||||
|
|
||||||
|
-- Cached last import validation result
|
||||||
|
QuestieLearnerExport.lastImportStats = nil
|
||||||
|
QuestieLearnerExport.lastImportData = nil
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Internal helpers
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function CountTable(t)
|
||||||
|
if not t then return 0 end
|
||||||
|
local n = 0
|
||||||
|
for _ in pairs(t) do n = n + 1 end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
local function GetServerKey()
|
||||||
|
if QuestieServer then
|
||||||
|
if Questie.IsAscension then return "Ascension" end
|
||||||
|
if Questie.IsTurtle then return "Turtle" end
|
||||||
|
if Questie.IsEbonhold then return "Ebonhold" end
|
||||||
|
if Questie.IsEra then return "Era" end
|
||||||
|
if Questie.Is335 then return "WotLK" end
|
||||||
|
end
|
||||||
|
local realm = GetRealmName and GetRealmName() or "unknown"
|
||||||
|
return realm ~= "" and realm or "unknown"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the learnedData sub-table for the current server, or nil
|
||||||
|
local function GetServerBucket(serverKey)
|
||||||
|
local ld = Questie.db and Questie.db.global and Questie.db.global.learnedData
|
||||||
|
if not ld then return nil end
|
||||||
|
if ld[serverKey] then return ld[serverKey] end
|
||||||
|
-- Fallback: flat (pre-bucket) layout still in use
|
||||||
|
if ld.npcs or ld.quests then return ld end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a lightweight stats summary table from a bucket
|
||||||
|
local function BuildStats(bucket)
|
||||||
|
if not bucket then return { npcs = 0, quests = 0, items = 0, objects = 0, total = 0 } end
|
||||||
|
local s = {
|
||||||
|
npcs = CountTable(bucket.npcs),
|
||||||
|
quests = CountTable(bucket.quests),
|
||||||
|
items = CountTable(bucket.items),
|
||||||
|
objects = CountTable(bucket.objects),
|
||||||
|
}
|
||||||
|
s.total = s.npcs + s.quests + s.items + s.objects
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Export
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- Serializes + deflates + encodes the learned data for the given server key.
|
||||||
|
--- Returns the export string and a stats table, or nil + error message.
|
||||||
|
---@param serverKey string|nil defaults to current server
|
||||||
|
---@return string|nil, table|string
|
||||||
|
function QuestieLearnerExport:Export(serverKey)
|
||||||
|
serverKey = serverKey or GetServerKey()
|
||||||
|
local bucket = GetServerBucket(serverKey)
|
||||||
|
if not bucket then
|
||||||
|
return nil, "No learned data found for server: " .. tostring(serverKey)
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = BuildStats(bucket)
|
||||||
|
if stats.total == 0 then
|
||||||
|
return nil, "Nothing to export — learned data is empty."
|
||||||
|
end
|
||||||
|
|
||||||
|
local payload = {
|
||||||
|
v = FORMAT_VERSION,
|
||||||
|
server = serverKey,
|
||||||
|
ts = time and time() or 0,
|
||||||
|
data = bucket,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ok, serialized = pcall(AceSerializer.Serialize, AceSerializer, payload)
|
||||||
|
if not ok or not serialized then
|
||||||
|
return nil, "Serialization failed: " .. tostring(serialized)
|
||||||
|
end
|
||||||
|
|
||||||
|
local compressed = LibDeflate:CompressDeflate(serialized, { level = 9 })
|
||||||
|
if not compressed then
|
||||||
|
return nil, "Compression failed."
|
||||||
|
end
|
||||||
|
|
||||||
|
local encoded = LibDeflate:EncodeForPrint(compressed)
|
||||||
|
if not encoded then
|
||||||
|
return nil, "Encoding failed."
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = FORMAT_PREFIX .. ":" .. FORMAT_VERSION .. FORMAT_SEP .. encoded
|
||||||
|
|
||||||
|
self.lastExportString = result
|
||||||
|
self.lastExportStats = stats
|
||||||
|
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport] Exported", stats.total,
|
||||||
|
"entries for", serverKey, "len:", string.len(result))
|
||||||
|
|
||||||
|
return result, stats
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Exports ALL server buckets merged into one payload.
|
||||||
|
---@return string|nil, table|string
|
||||||
|
function QuestieLearnerExport:ExportAll()
|
||||||
|
local ld = Questie.db and Questie.db.global and Questie.db.global.learnedData
|
||||||
|
if not ld then return nil, "No learned data." end
|
||||||
|
|
||||||
|
local merged = { npcs = {}, quests = {}, items = {}, objects = {} }
|
||||||
|
local function MergeBucket(b)
|
||||||
|
if not b then return end
|
||||||
|
for id, v in pairs(b.npcs or {}) do merged.npcs[id] = v end
|
||||||
|
for id, v in pairs(b.quests or {}) do merged.quests[id] = v end
|
||||||
|
for id, v in pairs(b.items or {}) do merged.items[id] = v end
|
||||||
|
for id, v in pairs(b.objects or {}) do merged.objects[id] = v end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flat layout
|
||||||
|
if ld.npcs or ld.quests then
|
||||||
|
MergeBucket(ld)
|
||||||
|
else
|
||||||
|
for _, bucket in pairs(ld) do
|
||||||
|
if type(bucket) == "table" and bucket.npcs then
|
||||||
|
MergeBucket(bucket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = BuildStats(merged)
|
||||||
|
if stats.total == 0 then return nil, "Nothing to export." end
|
||||||
|
|
||||||
|
local payload = {
|
||||||
|
v = FORMAT_VERSION,
|
||||||
|
server = "all",
|
||||||
|
ts = time and time() or 0,
|
||||||
|
data = merged,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ok, serialized = pcall(AceSerializer.Serialize, AceSerializer, payload)
|
||||||
|
if not ok or not serialized then
|
||||||
|
return nil, "Serialization failed."
|
||||||
|
end
|
||||||
|
|
||||||
|
local compressed = LibDeflate:CompressDeflate(serialized, { level = 9 })
|
||||||
|
if not compressed then return nil, "Compression failed." end
|
||||||
|
|
||||||
|
local encoded = LibDeflate:EncodeForPrint(compressed)
|
||||||
|
if not encoded then return nil, "Encoding failed." end
|
||||||
|
|
||||||
|
local result = FORMAT_PREFIX .. ":" .. FORMAT_VERSION .. FORMAT_SEP .. encoded
|
||||||
|
self.lastExportString = result
|
||||||
|
self.lastExportStats = stats
|
||||||
|
return result, stats
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Import / Validate
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- Validates an import string and returns the decoded payload table,
|
||||||
|
--- or nil + error string. Does NOT merge — call MergeImport() after confirming.
|
||||||
|
---@param importStr string
|
||||||
|
---@return table|nil, string|table
|
||||||
|
function QuestieLearnerExport:ValidateImport(importStr)
|
||||||
|
self.lastImportData = nil
|
||||||
|
self.lastImportStats = nil
|
||||||
|
|
||||||
|
if not importStr or importStr == "" then
|
||||||
|
return nil, "Empty import string."
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Strip whitespace
|
||||||
|
importStr = importStr:gsub("%s+", "")
|
||||||
|
|
||||||
|
-- Check prefix
|
||||||
|
if not importStr:sub(1, #FORMAT_PREFIX + 2) == FORMAT_PREFIX .. ":" then
|
||||||
|
return nil, "Not a Questie-X export string (missing QxLD prefix)."
|
||||||
|
end
|
||||||
|
|
||||||
|
local sepPos = importStr:find(FORMAT_SEP, 1, true)
|
||||||
|
if not sepPos then
|
||||||
|
return nil, "Malformed string — missing separator."
|
||||||
|
end
|
||||||
|
|
||||||
|
local encoded = importStr:sub(sepPos + 1)
|
||||||
|
if string.len(encoded) > MAX_IMPORT_LEN then
|
||||||
|
return nil, "Import string too large (max 512 KB)."
|
||||||
|
end
|
||||||
|
|
||||||
|
local compressed = LibDeflate:DecodeForPrint(encoded)
|
||||||
|
if not compressed then
|
||||||
|
return nil, "Decode failed — string may be corrupted."
|
||||||
|
end
|
||||||
|
|
||||||
|
local serialized = LibDeflate:DecompressDeflate(compressed)
|
||||||
|
if not serialized then
|
||||||
|
return nil, "Decompression failed — string may be corrupted."
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, payload = AceSerializer:Deserialize(serialized)
|
||||||
|
if not ok or type(payload) ~= "table" then
|
||||||
|
return nil, "Deserialization failed — data is malformed."
|
||||||
|
end
|
||||||
|
|
||||||
|
if payload.v ~= FORMAT_VERSION then
|
||||||
|
return nil, "Unsupported format version: " .. tostring(payload.v)
|
||||||
|
end
|
||||||
|
|
||||||
|
local bucket = payload.data
|
||||||
|
if type(bucket) ~= "table" then
|
||||||
|
return nil, "Payload missing data table."
|
||||||
|
end
|
||||||
|
if type(bucket.npcs) ~= "table" or
|
||||||
|
type(bucket.quests) ~= "table" or
|
||||||
|
type(bucket.items) ~= "table" or
|
||||||
|
type(bucket.objects) ~= "table" then
|
||||||
|
return nil, "Payload data is missing required sub-tables (npcs/quests/items/objects)."
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = BuildStats(bucket)
|
||||||
|
stats.server = payload.server or "unknown"
|
||||||
|
stats.ts = payload.ts or 0
|
||||||
|
|
||||||
|
self.lastImportData = payload
|
||||||
|
self.lastImportStats = stats
|
||||||
|
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport] ValidateImport OK:",
|
||||||
|
stats.total, "entries from", stats.server)
|
||||||
|
|
||||||
|
return payload, stats
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Merges previously validated import data into learnedData.
|
||||||
|
--- Must call ValidateImport() first.
|
||||||
|
---@return boolean, string
|
||||||
|
function QuestieLearnerExport:MergeImport()
|
||||||
|
if not self.lastImportData then
|
||||||
|
return false, "No validated import data. Run ValidateImport() first."
|
||||||
|
end
|
||||||
|
|
||||||
|
local payload = self.lastImportData
|
||||||
|
local bucket = payload.data
|
||||||
|
local merged = 0
|
||||||
|
local skipped = 0
|
||||||
|
|
||||||
|
local function MergeType(typ, src)
|
||||||
|
for id, d in pairs(src) do
|
||||||
|
local prevData = QuestieLearner.data
|
||||||
|
QuestieLearner:HandleNetworkData(typ, id, d)
|
||||||
|
if QuestieLearner.data ~= prevData then
|
||||||
|
merged = merged + 1
|
||||||
|
else
|
||||||
|
skipped = skipped + 1
|
||||||
|
end
|
||||||
|
merged = merged + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MergeType("NPC", bucket.npcs)
|
||||||
|
MergeType("QUEST", bucket.quests)
|
||||||
|
MergeType("ITEM", bucket.items)
|
||||||
|
MergeType("OBJECT", bucket.objects)
|
||||||
|
|
||||||
|
self.lastImportData = nil
|
||||||
|
self.lastImportStats = nil
|
||||||
|
|
||||||
|
local msg = "Import complete: merged " .. merged .. " entries, skipped " .. skipped .. " (already known)."
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport]", msg)
|
||||||
|
return true, msg
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
-- Cleanup / Prune
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- Returns a count of entries that would be pruned (dry run).
|
||||||
|
---@return table { npcs=N, quests=N, items=N, objects=N, total=N, reasons={} }
|
||||||
|
function QuestieLearnerExport:DryRunPrune()
|
||||||
|
return _Export:RunPrune(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Runs the actual prune and returns counts of removed entries.
|
||||||
|
---@return table
|
||||||
|
function QuestieLearnerExport:Prune()
|
||||||
|
return _Export:RunPrune(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
local QuestieDB -- lazily imported to avoid circular dep
|
||||||
|
|
||||||
|
function _Export:RunPrune(dryRun)
|
||||||
|
if not QuestieDB then QuestieDB = QuestieLoader:ImportModule("QuestieDB") end
|
||||||
|
|
||||||
|
local serverKey = GetServerKey()
|
||||||
|
local bucket = GetServerBucket(serverKey)
|
||||||
|
|
||||||
|
local result = { npcs = 0, quests = 0, items = 0, objects = 0, total = 0, reasons = {} }
|
||||||
|
if not bucket then return result end
|
||||||
|
|
||||||
|
local function ShouldPruneNPC(id, entry)
|
||||||
|
if CountTable(entry) == 0 then return "empty entry" end
|
||||||
|
if (entry.mc or 0) < 2 and not entry[7] then return "unverified with no coords" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ShouldPruneQuest(id, entry)
|
||||||
|
if CountTable(entry) == 0 then return "empty entry" end
|
||||||
|
if QuestieDB and QuestieDB.GetQuest then
|
||||||
|
local dbEntry = QuestieDB:GetQuest(id)
|
||||||
|
if dbEntry and (entry.mc or 0) < 2 then
|
||||||
|
return "fully covered by official DB, mc < 2"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ShouldPruneItem(id, entry)
|
||||||
|
if CountTable(entry) == 0 then return "empty entry" end
|
||||||
|
if (entry.mc or 0) < 1 then return "zero match count" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ShouldPruneObject(id, entry)
|
||||||
|
if CountTable(entry) == 0 then return "empty entry" end
|
||||||
|
if (entry.mc or 0) < 2 and not entry[4] then return "unverified with no coords" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function PruneStore(store, checkFn, typeName)
|
||||||
|
if not store then return end
|
||||||
|
for id, entry in pairs(store) do
|
||||||
|
local reason = checkFn(id, entry)
|
||||||
|
if reason then
|
||||||
|
result[typeName] = result[typeName] + 1
|
||||||
|
result.total = result.total + 1
|
||||||
|
table.insert(result.reasons, typeName .. ":" .. tostring(id) .. " — " .. reason)
|
||||||
|
if not dryRun then
|
||||||
|
store[id] = nil
|
||||||
|
end
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport] Prune",
|
||||||
|
dryRun and "(dry)" or "", typeName, id, reason)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PruneStore(bucket.npcs, ShouldPruneNPC, "npcs")
|
||||||
|
PruneStore(bucket.quests, ShouldPruneQuest, "quests")
|
||||||
|
PruneStore(bucket.items, ShouldPruneItem, "items")
|
||||||
|
PruneStore(bucket.objects, ShouldPruneObject, "objects")
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
@@ -213,6 +213,7 @@ Modules\Options\QuestieOptions.lua
|
|||||||
Modules\Options\QuestieOptionsDefaults.lua
|
Modules\Options\QuestieOptionsDefaults.lua
|
||||||
Modules\Options\QuestieOptionsUtils.lua
|
Modules\Options\QuestieOptionsUtils.lua
|
||||||
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
||||||
|
Modules\Options\DatabaseTab\QuestieOptionsDatabase.lua
|
||||||
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
||||||
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
||||||
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
||||||
@@ -229,3 +230,5 @@ Modules\QuestieProfiler.lua
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
Questie.lua
|
Questie.lua
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ Modules\Options\QuestieOptions.lua
|
|||||||
Modules\Options\QuestieOptionsDefaults.lua
|
Modules\Options\QuestieOptionsDefaults.lua
|
||||||
Modules\Options\QuestieOptionsUtils.lua
|
Modules\Options\QuestieOptionsUtils.lua
|
||||||
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
||||||
|
Modules\Options\DatabaseTab\QuestieOptionsDatabase.lua
|
||||||
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
||||||
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
||||||
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
||||||
@@ -220,3 +221,5 @@ Modules\QuestieProfiler.lua
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
Questie.lua
|
Questie.lua
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ Modules\Options\QuestieOptions.lua
|
|||||||
Modules\Options\QuestieOptionsDefaults.lua
|
Modules\Options\QuestieOptionsDefaults.lua
|
||||||
Modules\Options\QuestieOptionsUtils.lua
|
Modules\Options\QuestieOptionsUtils.lua
|
||||||
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
||||||
|
Modules\Options\DatabaseTab\QuestieOptionsDatabase.lua
|
||||||
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
||||||
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
||||||
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
||||||
@@ -235,3 +236,5 @@ Modules\QuestieMenu\Mailboxes.lua
|
|||||||
Modules\QuestieMenu\MeetingStones.lua
|
Modules\QuestieMenu\MeetingStones.lua
|
||||||
Modules\QuestieMenu\ProfessionTrainers.lua
|
Modules\QuestieMenu\ProfessionTrainers.lua
|
||||||
Modules\QuestieMenu\QuestieMenu.lua
|
Modules\QuestieMenu\QuestieMenu.lua
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ Modules\QuestieCoordinates.lua
|
|||||||
Modules\Network\QuestieComms.lua
|
Modules\Network\QuestieComms.lua
|
||||||
Modules\Network\QuestieCommsData.lua
|
Modules\Network\QuestieCommsData.lua
|
||||||
Modules\Network\QuestieLearnerComms.lua
|
Modules\Network\QuestieLearnerComms.lua
|
||||||
|
Modules\QuestieLearnerExport.lua
|
||||||
|
|
||||||
# Journey
|
# Journey
|
||||||
Modules\Journey\QuestieJourney.lua
|
Modules\Journey\QuestieJourney.lua
|
||||||
@@ -228,6 +229,7 @@ Modules\Options\QuestieOptions.lua
|
|||||||
Modules\Options\QuestieOptionsDefaults.lua
|
Modules\Options\QuestieOptionsDefaults.lua
|
||||||
Modules\Options\QuestieOptionsUtils.lua
|
Modules\Options\QuestieOptionsUtils.lua
|
||||||
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua
|
||||||
|
Modules\Options\DatabaseTab\QuestieOptionsDatabase.lua
|
||||||
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
Modules\Options\CreditsTab\QuestieOptionsCredits.lua
|
||||||
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
Modules\Options\AutoTab\QuestieOptionsAuto.lua
|
||||||
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
Modules\Options\DBMTab\QuestieOptionsDBM.lua
|
||||||
@@ -244,3 +246,5 @@ Modules\QuestieProfiler.lua
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
Questie.lua
|
Questie.lua
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user