diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e3a85..5719005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ ### Bug Fixes +- **[Learner - Robust Safety Fallbacks For All Types]** Hardened the learner's data-ingestion paths against malformed/partial data for every learner type (NPC/object/item/quest). The spawn validator now checks object spawn coordinates (key `[4]`) in addition to NPC spawns (key `[7]`) — previously object coords were unvalidated. The incoming-data 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 now isolates each broadcast in a `pcall`, so a single malformed entry can't abort the batch or break the live-update loop, and the post-merge `InjectLearnedData` is likewise guarded. - **[Learner - Hardened Import/Merge For Multi-Player Data]** Importing learned data merged from several different players is now safe and clean. Fixed a prefix-validation bug in `ValidateImport` (`not str:sub(...) == x` parsed as `(not str:sub(...)) == x`, always false, so non-Questie strings were never rejected). `MergeImport` now: ensures the learner stores exist (so a fresh profile can import), merges each entry **synchronously and defensively** via `_ApplyIncomingNetworkMerge` (which validates key/coordinate structure and only adopts fields the local store is missing — never overwriting good local data), isolates every entry in a `pcall` so one corrupt entry is skipped/counted instead of aborting the import or corrupting the store, and reports accurate `merged / skipped / rejected` counts. Merging synchronously also fixes `InjectLearnedData` previously running before the queued merges landed. - **[Quest - Turn-In '?' Missing For Quests In The Log]** A quest in the player's log is active and not yet turned in, so its turn-in `?` finisher should always be drawable — but `AddFinisher` also required `not char.complete[questId]`, so a quest completed in a previous Ascension prestige (still flagged in `char.complete`) and then re-accepted had its finisher suppressed while standing at the turn-in NPC. `AddFinisher` now trusts the live quest log: if the quest is in `currentQuestlog` and not failed, the finisher draws regardless of the (possibly stale) completed flag. No quest-completion data is modified. - **[Learner - Turn-In/Quest NPC Not Learned When Targeted]** In learner mode the turn-in NPC's location wasn't always recorded, so its `?` finisher couldn't draw. `OnTargetChanged` only cached the GUID and never called `LearnNPC`; it now learns the spawn of quest-giver/turn-in NPCs (gated like `OnMouseoverUnit`), so simply targeting a turn-in NPC records its position. The `QUEST_COMPLETE`/`QUEST_TURNED_IN` handlers also fall back to the `target` unit when the `npc` gossip unit is already cleared, so the finisher NPC is reliably learned on turn-in. diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index b7d2931..19d6316 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -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