feat(learner): zone-scoped export; 200k entry cap; SmoothReset after import
QuestieLearnerExport: - Extract _Encode helper to consolidate serialize+deflate+encode - Add ExportZone(serverKey, zoneId) to export only the learned data for a specific zone (NPCs/objects with spawns in that zone, plus their dropped items and related quests) for smaller, faster exports - Cap ValidateImport at 200k entries to guard against pathological payloads that could stall the client during merge - Call SmoothReset after MergeImport so newly imported spawns appear on the map without requiring /reload QuestieOptionsDatabase: - Refresh AceConfigRegistry after import so Database tab stats update immediately (previously required /reload or opening Options) - Add Export Current Zone button tied to ExportZone()
This commit is contained in:
@@ -197,6 +197,12 @@ _OpenImportDialog = function()
|
|||||||
if ok then
|
if ok then
|
||||||
statusLabel:SetText("|cFF00FF00" .. msg .. "|r")
|
statusLabel:SetText("|cFF00FF00" .. msg .. "|r")
|
||||||
importBtn:SetDisabled(true)
|
importBtn:SetDisabled(true)
|
||||||
|
-- Refresh the Database tab so the live stats counts update immediately
|
||||||
|
-- (the map/minimap is already redrawn inside MergeImport via SmoothReset).
|
||||||
|
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
|
||||||
|
if AceConfigRegistry and AceConfigRegistry.NotifyChange then
|
||||||
|
AceConfigRegistry:NotifyChange("Questie")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
statusLabel:SetText("|cFFFF0000" .. msg .. "|r")
|
statusLabel:SetText("|cFFFF0000" .. msg .. "|r")
|
||||||
end
|
end
|
||||||
@@ -411,6 +417,26 @@ function QuestieOptions.tabs.database:Initialize()
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
export_zone_btn = {
|
||||||
|
type = "execute",
|
||||||
|
order = 3.25,
|
||||||
|
name = function() return l10n("Export Current Zone") end,
|
||||||
|
desc = function() return "Export only the learned data for the zone you are standing in (NPCs/objects in the zone, plus the items they drop and quests they give)." end,
|
||||||
|
func = function()
|
||||||
|
local Exp = GetExportModule()
|
||||||
|
if not Exp then
|
||||||
|
Questie:Print("|cFFFF0000QuestieLearnerExport module not loaded.|r")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local str, statsOrErr = Exp:ExportZone()
|
||||||
|
if not str then
|
||||||
|
Questie:Print("|cFFFF0000Export failed: " .. tostring(statsOrErr) .. "|r")
|
||||||
|
else
|
||||||
|
_OpenExportDialog(str, statsOrErr)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
export_all_btn = {
|
export_all_btn = {
|
||||||
type = "execute",
|
type = "execute",
|
||||||
order = 3.3,
|
order = 3.3,
|
||||||
|
|||||||
@@ -78,31 +78,22 @@ end
|
|||||||
-- Export
|
-- Export
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
--- Serializes + deflates + encodes the learned data for the given server key.
|
--- Shared serialize + deflate + encode step. Wrapped defensively so any
|
||||||
--- Returns the export string and a stats table, or nil + error message.
|
--- malformed sub-entry surfaces as a clean error rather than a Lua error.
|
||||||
---@param serverKey string|nil defaults to current server
|
---@param serverKey string
|
||||||
|
---@param data table the bucket (npcs/quests/items/objects) to encode
|
||||||
|
---@param stats table
|
||||||
---@return string|nil, table|string
|
---@return string|nil, table|string
|
||||||
function QuestieLearnerExport:Export(serverKey)
|
function QuestieLearnerExport:_Encode(serverKey, data, stats)
|
||||||
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 = {
|
local payload = {
|
||||||
v = FORMAT_VERSION,
|
v = FORMAT_VERSION,
|
||||||
server = serverKey,
|
server = serverKey,
|
||||||
ts = time and time() or 0,
|
ts = time and time() or 0,
|
||||||
data = bucket,
|
data = data,
|
||||||
}
|
}
|
||||||
|
|
||||||
local ok, serialized = pcall(AceSerializer.Serialize, AceSerializer, payload)
|
local ok, serialized = pcall(AceSerializer.Serialize, AceSerializer, payload)
|
||||||
if not ok or not serialized then
|
if not ok or type(serialized) ~= "string" then
|
||||||
return nil, "Serialization failed: " .. tostring(serialized)
|
return nil, "Serialization failed: " .. tostring(serialized)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -121,12 +112,121 @@ function QuestieLearnerExport:Export(serverKey)
|
|||||||
self.lastExportString = result
|
self.lastExportString = result
|
||||||
self.lastExportStats = stats
|
self.lastExportStats = stats
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport] Exported", stats.total,
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[LearnerExport] Encoded", stats.total,
|
||||||
"entries for", serverKey, "len:", string.len(result))
|
"entries for", serverKey, "len:", string.len(result))
|
||||||
|
|
||||||
return result, stats
|
return result, stats
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- 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
|
||||||
|
|
||||||
|
return self:_Encode(serverKey, bucket, stats)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Exports only the learned entries that pertain to a single zone (areaId).
|
||||||
|
--- Includes NPCs/objects that spawn in the zone, plus items dropped by those
|
||||||
|
--- NPCs and quests started/finished by those NPCs/objects, so the bundle stays
|
||||||
|
--- referentially consistent.
|
||||||
|
---@param zoneId number areaId (defaults to the player's current zone)
|
||||||
|
---@param serverKey string|nil
|
||||||
|
---@return string|nil, table|string
|
||||||
|
function QuestieLearnerExport:ExportZone(zoneId, serverKey)
|
||||||
|
serverKey = serverKey or GetServerKey()
|
||||||
|
|
||||||
|
if not zoneId then
|
||||||
|
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
|
||||||
|
if QuestiePlayer and QuestiePlayer.GetCurrentZoneId then
|
||||||
|
zoneId = QuestiePlayer:GetCurrentZoneId()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(zoneId) ~= "number" or zoneId <= 0 then
|
||||||
|
return nil, "Could not determine your current zone. Stand in a known zone and try again."
|
||||||
|
end
|
||||||
|
|
||||||
|
local bucket = GetServerBucket(serverKey)
|
||||||
|
if not bucket then
|
||||||
|
return nil, "No learned data found for server: " .. tostring(serverKey)
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = { npcs = {}, quests = {}, items = {}, objects = {} }
|
||||||
|
local npcSet, objSet = {}, {}
|
||||||
|
|
||||||
|
local function ZoneHasCoords(spawns)
|
||||||
|
if type(spawns) ~= "table" then return false end
|
||||||
|
local zoneCoords = spawns[zoneId]
|
||||||
|
return type(zoneCoords) == "table" and next(zoneCoords) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- NPCs spawning in the zone (NPC spawns live at key [7])
|
||||||
|
for id, d in pairs(bucket.npcs or {}) do
|
||||||
|
if type(d) == "table" and ZoneHasCoords(d[7]) then
|
||||||
|
out.npcs[id] = d
|
||||||
|
npcSet[id] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Objects spawning in the zone (object spawns live at key [4])
|
||||||
|
for id, d in pairs(bucket.objects or {}) do
|
||||||
|
if type(d) == "table" and ZoneHasCoords(d[4]) then
|
||||||
|
out.objects[id] = d
|
||||||
|
objSet[id] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Items dropped by an included NPC (item [2] = dropNpcs array)
|
||||||
|
for id, d in pairs(bucket.items or {}) do
|
||||||
|
if type(d) == "table" and type(d[2]) == "table" then
|
||||||
|
for _, npcId in ipairs(d[2]) do
|
||||||
|
if npcSet[npcId] then
|
||||||
|
out.items[id] = d
|
||||||
|
break -- one match is enough
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Quests started/finished by an included NPC or object
|
||||||
|
-- (quest [2]=startedBy{npcIds,objIds,...}, [3]=finishedBy{npcIds,objIds})
|
||||||
|
local function AnyIn(list, set)
|
||||||
|
if type(list) ~= "table" then return false end
|
||||||
|
for _, v in ipairs(list) do
|
||||||
|
if set[v] then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for id, d in pairs(bucket.quests or {}) do
|
||||||
|
if type(d) == "table" then
|
||||||
|
local started, finished = d[2], d[3]
|
||||||
|
if (type(started) == "table" and (AnyIn(started[1], npcSet) or AnyIn(started[2], objSet)))
|
||||||
|
or (type(finished) == "table" and (AnyIn(finished[1], npcSet) or AnyIn(finished[2], objSet))) then
|
||||||
|
out.quests[id] = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = BuildStats(out)
|
||||||
|
stats.zone = zoneId
|
||||||
|
if stats.total == 0 then
|
||||||
|
return nil, "Nothing learned in this zone yet."
|
||||||
|
end
|
||||||
|
|
||||||
|
return self:_Encode(serverKey, out, stats)
|
||||||
|
end
|
||||||
|
|
||||||
--- Exports ALL server buckets merged into one payload.
|
--- Exports ALL server buckets merged into one payload.
|
||||||
---@return string|nil, table|string
|
---@return string|nil, table|string
|
||||||
function QuestieLearnerExport:ExportAll()
|
function QuestieLearnerExport:ExportAll()
|
||||||
@@ -156,28 +256,7 @@ function QuestieLearnerExport:ExportAll()
|
|||||||
local stats = BuildStats(merged)
|
local stats = BuildStats(merged)
|
||||||
if stats.total == 0 then return nil, "Nothing to export." end
|
if stats.total == 0 then return nil, "Nothing to export." end
|
||||||
|
|
||||||
local payload = {
|
return self:_Encode("all", merged, stats)
|
||||||
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
|
end
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
@@ -246,6 +325,11 @@ function QuestieLearnerExport:ValidateImport(importStr)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local stats = BuildStats(bucket)
|
local stats = BuildStats(bucket)
|
||||||
|
-- Guard against pathological payloads (e.g. a hand-crafted string with millions
|
||||||
|
-- of entries) that could stall the client during the synchronous merge.
|
||||||
|
if stats.total > 200000 then
|
||||||
|
return nil, "Import rejected — too many entries (" .. stats.total .. ")."
|
||||||
|
end
|
||||||
stats.server = payload.server or "unknown"
|
stats.server = payload.server or "unknown"
|
||||||
stats.ts = payload.ts or 0
|
stats.ts = payload.ts or 0
|
||||||
|
|
||||||
@@ -326,6 +410,13 @@ function QuestieLearnerExport:MergeImport()
|
|||||||
pcall(QuestieLearner.InjectLearnedData, QuestieLearner)
|
pcall(QuestieLearner.InjectLearnedData, QuestieLearner)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Redraw the map/minimap/tooltips so freshly imported spawns appear live
|
||||||
|
-- (without a /reload). SmoothReset clears and recalculates all notes.
|
||||||
|
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
|
||||||
|
if QuestieQuest and QuestieQuest.SmoothReset then
|
||||||
|
pcall(QuestieQuest.SmoothReset, QuestieQuest)
|
||||||
|
end
|
||||||
|
|
||||||
local msg = string.format("Import complete: merged %d, skipped %d already-known%s.",
|
local msg = string.format("Import complete: merged %d, skipped %d already-known%s.",
|
||||||
merged, skipped,
|
merged, skipped,
|
||||||
rejected > 0 and (", rejected " .. rejected .. " malformed") or "")
|
rejected > 0 and (", rejected " .. rejected .. " malformed") or "")
|
||||||
|
|||||||
Reference in New Issue
Block a user