fix(learner): robust safety fallbacks for all learner types

Hardens the learner data-ingestion paths against malformed/partial data
across NPC/object/item/quest types:

- _ValidateLearnedSpawnData now validates object spawn coordinates (key 4)
  in addition to NPC spawns (key 7); object coords were previously
  unvalidated. Extracted a shared _ValidateCoordTable helper.
- _ApplyIncomingNetworkMerge coordinate merge skips malformed zone keys and
  coordinates (non-number / out-of-range) per entry instead of erroring,
  and guards the item drop-list merge against non-number NPC ids.
- The live comms-merge flush isolates each broadcast in a pcall so one
  malformed entry can't abort the batch or break the live-update loop;
  the post-merge InjectLearnedData is also guarded.

Live Learn* entry points already validate id/type. selene 0 errors;
busted 145 successes / same 7 pre-existing failures.
This commit is contained in:
Xurkon
2026-06-10 05:16:51 -05:00
parent 2c2e4a24a7
commit aa50404484
2 changed files with 42 additions and 20 deletions
+41 -20
View File
@@ -5022,13 +5022,10 @@ end
--- in each zone are numbers within 0-100 range.
---@param data table The learned entity data table (e.g. NPC entry)
---@return boolean True if valid, false if malformed
local function _ValidateLearnedSpawnData(data)
if type(data) ~= "table" then return false end
local spawns = data[7]
if not spawns then return true end -- no spawn data is OK
-- Validates a single coordinate table (zoneId -> { {x,y}, ... }). Absent is OK.
local function _ValidateCoordTable(spawns)
if spawns == nil then return true end
if type(spawns) ~= "table" then return false end
for zoneId, zoneSpawns in pairs(spawns) do
if type(zoneId) ~= "number" then return false end
if type(zoneSpawns) ~= "table" then return false end
@@ -5042,6 +5039,15 @@ local function _ValidateLearnedSpawnData(data)
return true
end
local function _ValidateLearnedSpawnData(data)
if type(data) ~= "table" then return false end
-- NPC spawns live in key [7], object spawns in key [4]. Validate whichever is present
-- so malformed object coordinates are rejected too (the old check only covered [7]).
if not _ValidateCoordTable(data[7]) then return false end
if not _ValidateCoordTable(data[4]) then return false end
return true
end
function _Learner:BroadcastIfCommsAvailable(typ, id, data)
if Questie and Questie.db and Questie.db.profile and Questie.db.profile.learnerBroadcast == false then
return
@@ -5119,12 +5125,20 @@ local function _QueueIncomingNetworkMerge(typ, id, data, op)
local anyChanged = false
for _, entry in pairs(pending) do
local changed = QuestieLearner:_ApplyIncomingNetworkMerge(entry.typ, entry.id, entry.data, entry.op)
anyChanged = anyChanged or changed
-- Isolate each entry: a single malformed broadcast must not abort the flush
-- and lose the rest of the batch (or break the live update loop).
local ok, changed = pcall(QuestieLearner._ApplyIncomingNetworkMerge, QuestieLearner,
entry.typ, entry.id, entry.data, entry.op)
if ok then
anyChanged = anyChanged or changed
else
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Skipped malformed network merge",
tostring(entry.typ), tostring(entry.id), "-", tostring(changed))
end
end
if anyChanged then
QuestieLearner:InjectLearnedData()
pcall(QuestieLearner.InjectLearnedData, QuestieLearner)
QuestieLearner.data = Questie.dbLearner.global
end
end
@@ -5206,10 +5220,15 @@ function QuestieLearner:_ApplyIncomingNetworkMerge(typ, id, d, op)
existing[coordKey] = existing[coordKey] or {}
local grid = GetCustomGridPrecision()
for zoneId, coords in pairs(d[coordKey]) do
existing[coordKey][zoneId] = existing[coordKey][zoneId] or {}
for _, coord in ipairs(coords) do
if InsertIfNewBucket(existing[coordKey][zoneId], coord[1], coord[2], grid) then
changed = true
-- Defensive: skip malformed zone keys / coord lists rather than erroring.
if type(zoneId) == "number" and type(coords) == "table" then
existing[coordKey][zoneId] = existing[coordKey][zoneId] or {}
for _, coord in ipairs(coords) do
if type(coord) == "table" and type(coord[1]) == "number" and type(coord[2]) == "number" then
if InsertIfNewBucket(existing[coordKey][zoneId], coord[1], coord[2], grid) then
changed = true
end
end
end
end
end
@@ -5219,13 +5238,15 @@ function QuestieLearner:_ApplyIncomingNetworkMerge(typ, id, d, op)
if typ == "ITEM" and type(d[2]) == "table" then
existing[2] = existing[2] or {}
for _, npcId in ipairs(d[2]) do
local found = false
for _, existId in ipairs(existing[2]) do
if existId == npcId then found = true; break end
end
if not found then
table.insert(existing[2], npcId)
changed = true
if type(npcId) == "number" then
local found = false
for _, existId in ipairs(existing[2]) do
if existId == npcId then found = true; break end
end
if not found then
table.insert(existing[2], npcId)
changed = true
end
end
end
end