feat: release v1.3.0 - QuestieLearner Confidence & Tiered Pruning Engine

This commit is contained in:
Xurkon
2026-03-17 22:33:01 -05:00
parent 7f16d31294
commit b0f8ba22e7
20 changed files with 1540 additions and 199 deletions
+4
View File
@@ -13,3 +13,7 @@ __pycache__/
coords.lua
debug.lua
debug_tooltip.lua
tmp_*.py
.history/
Research/
Tools/
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## v1.3.0 — QuestieLearner Confidence, Global Sharing & Stale Data Cleanup
### QuestieLearner.lua — Precision & Confidence
- **[Coordinate Scaling Fix]** Fixed player coordinates being recorded on a 0-1 scale; now correctly scales to 0-100 for compatibility with Questie map pins.
- **[Confidence Rating System]** Introduced a confidence system based on "Match Count" (`mc`). Data is now categorized as "Unconfirmed" (low confidence) or "Verified" (high confidence).
- **[Map Pin Gating]** Learned map pins (sword icons) now only appear after reaching a configurable confidence threshold (default: 2).
- **[Confidence in Tooltips]** NPC and Object tooltips now display their confidence level (e.g., `(Learned - Confidence: 2)`).
- **[Timestamp Tracking]** Added `lastSeen` (`ls`) timestamps to all learned entries to track data freshness.
### QuestieLearnerComms.lua — Global Data Sharing
- **[Community Reinforcement]** Expanded data sharing from Party/Guild to a global hidden channel. Confidence values (`mc`) now increment when identical data is received from other Questie users, allowing the community to verify spawns collectively.
- **[Network Freshness]** Receiving data over the network now refreshes the `lastSeen` timestamp, keeping active community spawns from being pruned.
### QuestieLearnerExport.lua — Tiered Stale Data Cleanup
- **[Tiered Pruning]** Implemented a robust cleanup system that protects "Verified" (high-confidence) data from age-based deletion.
- **[Age-Based Pruning]** "Unconfirmed" data is now automatically pruned if it hasn't been seen within a configurable timeframe (default: 90 days).
- **[Redundancy Pruning]** Logic to remove data already present in the official Questie database now respects the `pruneVerified` toggle, allowing users to keep verified personal data even if it overlaps with the core DB.
### QuestieOptionsDatabase.lua — Advanced Cleanup Controls
- **[Stale Data Threshold]** Added a slider to control the age-pruning threshold (1-180 days) for unconfirmed data.
- **[Verified Data Protection]** Added a toggle to include or exclude verified data from redundancy pruning.
- **[UI Reorganization]** Refactored the Database tab's cleanup section for better logical flow and clarity.
---
## v1.2.9 — QuestieLearner Cross-Link Engine + Tracker Zone Fix + Untrack Fix
### QuestieLearner.lua — Universal Cross-Link Engine
+36
View File
@@ -586,6 +586,42 @@ function QuestieDB.IsParentQuestActive(parentID)
return false
end
--- Returns a table of [npcId] = true for NPCs that have verified learned data (Confidence >= 2)
--- in a specific zone. Used to hide static database spawns in favor of verified ones.
---@param zoneId number
---@return table<number, boolean>
function QuestieDB.GetSuppressedNPCs(zoneId)
local suppressed = {}
local ld = Questie.db.global.learnedData
if ld and ld.settings and ld.settings.prioritizeMyData and ld.npcs then
local threshold = ld.settings.minConfidencePins or 2
for npcId, entry in pairs(ld.npcs) do
if entry.mc and entry.mc >= threshold and entry[7] and entry[7][zoneId] then
suppressed[npcId] = true
end
end
end
return suppressed
end
--- Returns a table of [objectId] = true for Objects that have verified learned data (Confidence >= 2)
--- in a specific zone. Used to hide static database spawns in favor of verified ones.
---@param zoneId number
---@return table<number, boolean>
function QuestieDB.GetSuppressedObjects(zoneId)
local suppressed = {}
local ld = Questie.db.global.learnedData
if ld and ld.settings and ld.settings.prioritizeMyData and ld.objects then
local threshold = ld.settings.minConfidencePins or 2
for objId, entry in pairs(ld.objects) do
if entry.mc and entry.mc >= threshold and entry[4] and entry[4][zoneId] then
suppressed[objId] = true
end
end
end
return suppressed
end
---@param preQuestGroup table<number, number>
---@return boolean
function QuestieDB:IsPreQuestGroupFulfilled(preQuestGroup)
+13 -4
View File
@@ -11,7 +11,16 @@ local AceComm = LibStub("AceComm-3.0")
local addonPrefix = "QuestieLearner"
local hiddenChannelName = "questiecomm"
local ProtocolVersion = 1
local ProtocolVersion = 2 -- Increment protocol version for enhanced/sanitized data
local time = time
local GetTime = GetTime
local math_min = math.min
local math_floor = math.floor
local math_random = math.random
local table_insert = table.insert
local table_remove = table.remove
local table_getn = table.getn
-- Dev Logging Flags — defined first so all functions below can call DebugLog
local LOG_CRITICAL = true
@@ -123,10 +132,10 @@ function QuestieLearnerComms:Initialize()
end
-- Process incoming/outgoing queues
C_Timer.NewTicker(0.2, function() _QuestieLearnerComms:ProcessQueues() end)
QuestieCompat.C_Timer.NewTicker(0.2, function() _QuestieLearnerComms:ProcessQueues() end)
-- Start Reinforcement Loop (every 60 seconds)
C_Timer.NewTicker(60, function() _QuestieLearnerComms:ProcessReinforcement() end)
QuestieCompat.C_Timer.NewTicker(60, function() _QuestieLearnerComms:ProcessReinforcement() end)
end
function _QuestieLearnerComms:ProcessReinforcement()
@@ -270,5 +279,5 @@ function _QuestieLearnerComms:ProcessRawMessage(encodedMsg, sender)
DebugLog("DEVELOP", "Received " .. tostring(op) .. " " .. tostring(typ) .. " " .. tostring(id) .. " from " .. tostring(sender))
QuestieLearner:HandleNetworkData(typ, id, d)
QuestieLearner:HandleNetworkData(typ, id, d, op)
end
@@ -421,19 +421,7 @@ function QuestieOptions.tabs.advanced:Initialize()
order = 6,
name = l10n('3.3.5 Compatibility Settings'),
},
plugin_header = {
type = "header",
order = 7,
name = "|cFF5EBAF3Loaded Questie-X Plugins|r",
},
plugin_status_desc = {
type = "description",
order = 7.01,
fontSize = "medium",
name = function()
return "|cFF888888Plugin stats have moved to the |r|cFFFFFFFFDatabase|r|cFF888888 tab.|r"
end,
},
},
}
end
@@ -366,9 +366,34 @@ function QuestieOptions.tabs.database:Initialize()
end,
},
stale_threshold = {
type = "range",
order = 5.2,
name = function() return l10n("Stale Data Threshold (Days)") end,
desc = function() return l10n("Unconfirmed learned data (seen only once) will be pruned if it hasn't been seen in this many days. Verified data is permanent.") end,
min = 1,
max = 180,
step = 1,
get = function() return (Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.staleThreshold) or 90 end,
set = function(_, val)
Questie.db.global.learnedData.settings.staleThreshold = val
end,
},
prune_verified = {
type = "toggle",
order = 5.3,
name = function() return l10n("Include Verified Data in Pruning") end,
desc = function() return l10n("If enabled, even high-confidence (Verified) data will be subject to redundancy pruning (e.g., if it's already in the official DB). Time-based pruning still only affects unconfirmed data.") end,
get = function() return (Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.pruneVerified) or false end,
set = function(_, val)
Questie.db.global.learnedData.settings.pruneVerified = val
end,
},
prune_dry_btn = {
type = "execute",
order = 5.2,
order = 5.4,
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()
@@ -387,7 +412,7 @@ function QuestieOptions.tabs.database:Initialize()
prune_btn = {
type = "execute",
order = 5.3,
order = 5.5,
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()
@@ -403,7 +428,7 @@ function QuestieOptions.tabs.database:Initialize()
prune_all_btn = {
type = "execute",
order = 5.4,
order = 5.6,
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,
+26 -1
View File
@@ -985,6 +985,8 @@ function QuestieQuest:UpdateObjectiveNotes(quest)
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] UpdateObjectiveNotes:", quest.Id)
for objectiveIndex, objective in pairs(quest.Objectives) do
-- Skip tracker-only fallback objectives — they have no DB Id and can't be populated
if objective.Type ~= "fallback" then
local result, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, objectiveIndex,
objective, false)
if (not result) then
@@ -992,9 +994,11 @@ function QuestieQuest:UpdateObjectiveNotes(quest)
quest.name, quest.Id, objectiveIndex, err)
end
end
end
if quest.SpecialObjectives and next(quest.SpecialObjectives) then
for _, objective in pairs(quest.SpecialObjectives) do
if objective.Type ~= "fallback" then
local result, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, 0, objective,
true)
if not result then
@@ -1004,6 +1008,7 @@ function QuestieQuest:UpdateObjectiveNotes(quest)
end
end
end
end
end
-- This function is used to check the players bags for an item that matches quest.sourceItemId.
@@ -1318,6 +1323,26 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
objectiveCenter = { x = x, y = y }
end
-- Filter static spawns if prioritizeMyData is enabled and we have high-confidence learned data
if Questie.db.global.learnedData and Questie.db.global.learnedData.settings and Questie.db.global.learnedData.settings.prioritizeMyData then
for zone in pairs(zones) do
local suppressed = (objectiveData.Type == "monster" and QuestieDB.GetSuppressedNPCs(zone)) or (objectiveData.Type == "object" and QuestieDB.GetSuppressedObjects(zone))
if suppressed then
for id, spawnData in pairs(objective.spawnList) do
if suppressed[id] and spawnData.Spawns and spawnData.Spawns[zone] then
-- Only suppress if this isn't a learned spawn (learned spawns have .isLearned)
if not spawnData.isLearned then
spawnData.Spawns[zone] = nil
if not next(spawnData.Spawns) then
objective.spawnList[id] = nil
end
end
end
end
end
end
end
local iconsToDraw, _ = _DetermineIconsToDraw(quest, objective, objectiveIndex, objectiveCenter)
local icon, iconPerZone = _DrawObjectiveIcons(quest.Id, iconsToDraw, objective, maxPerType)
_DrawObjectiveWaypoints(objective, icon, iconPerZone)
@@ -1341,7 +1366,7 @@ _RegisterObjectiveTooltips = function(objective, questId, blockItemTooltips)
-- No spawnList and no Id means there is nothing Questie can draw for this objective.
-- This covers server-tracked trigger objectives (e.g. "complete N quests in zone" for
-- quest 50150) which may have any objectiveType from the server, not just "event".
if not objective.Id then
if not objective.Id or objective.Id == 0 then
objective.hasRegisteredTooltips = true
return
end
File diff suppressed because it is too large Load Diff
+32 -1
View File
@@ -326,32 +326,63 @@ function _Export:RunPrune(dryRun)
local result = { npcs = 0, quests = 0, items = 0, objects = 0, total = 0, reasons = {} }
if not bucket then return result end
local settings = (Questie.db.global.learnedData and Questie.db.global.learnedData.settings) or {}
local thresholdDays = settings.staleThreshold or 90
local thresholdSeconds = thresholdDays * 86400
local minConfidence = settings.minConfidencePins or 2
local pruneVerified = settings.pruneVerified
local now = time()
local function ShouldPruneNPC(id, entry)
if CountTable(entry) == 0 then return "empty entry" end
local isVerified = (entry.mc or 0) >= minConfidence
if (not isVerified) and (now - (entry.ls or 0)) > thresholdSeconds then
return "unconfirmed and stale (> " .. thresholdDays .. " days)"
end
if pruneVerified or not isVerified then
if (entry.mc or 0) < 2 and not entry[7] then return "unverified with no coords" end
end
return nil
end
local function ShouldPruneQuest(id, entry)
if CountTable(entry) == 0 then return "empty entry" end
local isVerified = (entry.mc or 0) >= minConfidence
if (not isVerified) and (now - (entry.ls or 0)) > thresholdSeconds then
return "unconfirmed and stale (> " .. thresholdDays .. " days)"
end
if pruneVerified or not isVerified then
if QuestieDB and QuestieDB.GetQuest then
local dbEntry = QuestieDB:GetQuest(id)
local dbEntry = QuestieDB.GetQuest(id)
if dbEntry and (entry.mc or 0) < 2 then
return "fully covered by official DB, mc < 2"
end
end
end
return nil
end
local function ShouldPruneItem(id, entry)
if CountTable(entry) == 0 then return "empty entry" end
local isVerified = (entry.mc or 0) >= minConfidence
if (not isVerified) and (now - (entry.ls or 0)) > thresholdSeconds then
return "unconfirmed and stale (> " .. thresholdDays .. " days)"
end
if pruneVerified or not isVerified then
if (entry.mc or 0) < 1 then return "zero match count" end
end
return nil
end
local function ShouldPruneObject(id, entry)
if CountTable(entry) == 0 then return "empty entry" end
local isVerified = (entry.mc or 0) >= minConfidence
if (not isVerified) and (now - (entry.ls or 0)) > thresholdSeconds then
return "unconfirmed and stale (> " .. thresholdDays .. " days)"
end
if pruneVerified or not isVerified then
if (entry.mc or 0) < 2 and not entry[4] then return "unverified with no coords" end
end
return nil
end
+59 -1
View File
@@ -219,6 +219,64 @@ function QuestieTooltips:GetTooltip(key)
local tooltipData = {}
local tooltipLines = {}
if (not QuestieTooltips.lookupByKey[key]) then
local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner")
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache")
if QuestieLearner and QuestieLearner.data then
-- Try to find in learned NPCs or objects
local id = tonumber(key:sub(3))
if id then
if key:sub(1,2) == "m_" then
local learnedNpc = QuestieLearner.data.npcs[id]
if learnedNpc and learnedNpc[10] then -- check questObjectives
for questId, objList in pairs(learnedNpc[10]) do
for _, objText in ipairs(objList) do
local needed, collected
local objectives = QuestLogCache.GetQuestObjectives(questId)
if objectives then
for _, obj in pairs(objectives) do
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
needed = obj.numRequired
collected = obj.numFulfilled
break
end
end
end
QuestieTooltips:RegisterObjectiveTooltip(questId, key, { Index = 0, Description = objText, Needed = needed, Collected = collected, Update = function() end })
end
end
if learnedNpc.mc then
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedNpc.mc) .. ")|r")
end
end
elseif key:sub(1,2) == "o_" then
local learnedObj = QuestieLearner.data.objects[id]
if learnedObj and learnedObj[10] then
for questId, objList in pairs(learnedObj[10]) do
for _, objText in ipairs(objList) do
local needed, collected
local objectives = QuestLogCache.GetQuestObjectives(questId)
if objectives then
for _, obj in pairs(objectives) do
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
needed = obj.numRequired
collected = obj.numFulfilled
break
end
end
end
QuestieTooltips:RegisterObjectiveTooltip(questId, key, { Index = 0, Description = objText, Needed = needed, Collected = collected, Update = function() end })
end
end
if learnedObj.mc then
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedObj.mc) .. ")|r")
end
end
end
end
end
end
if QuestieTooltips.lookupByKey[key] then
local playerName = UnitName("player")
for k, tooltip in pairs(QuestieTooltips.lookupByKey[key]) do
@@ -229,7 +287,7 @@ function QuestieTooltips:GetTooltip(key)
end
else
local objective = tooltip.objective
if not (objective.IsSourceItem or objective.IsRequiredSourceItem) then
if objective and not (objective.IsSourceItem or objective.IsRequiredSourceItem) and objective.Update then
-- Tooltip was registered for a sourceItem or requiredSourceItem and not a real "objective"
objective:Update()
end
+19 -7
View File
@@ -914,7 +914,17 @@ function QuestieTracker:Update()
-- Set Quest Title - This handles the "Auto Minimize Completed Quests" option but we don't auto-minimize timed quests.
local coloredQuestName
if timedQuest then
if quest.isFallback or quest._isLogFallback then
-- Quest not in DB: use the name stored on the fallback object
local questName = quest.name or tostring(quest.Id)
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
questName = "[" .. quest.level .. "] " .. questName
end
if Questie.db.profile.enableTooltipsQuestID then
questName = questName .. " (" .. quest.Id .. ")"
end
coloredQuestName = "|cFFFFFF00" .. questName .. "|r"
elseif timedQuest then
coloredQuestName = QuestieLib:GetColoredQuestName(quest.Id,
Questie.db.profile.trackerShowQuestLevel, false, false)
else
@@ -2290,10 +2300,15 @@ end
function QuestieTracker:UntrackQuestId(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UntrackQuestId] - ", questId)
-- Always remove from tracked quests when manually untracking
if Questie.db.profile.autoTrackQuests then
-- In auto-track mode, mark the quest as explicitly hidden
Questie.db.char.AutoUntrackedQuests[questId] = true
Questie.db.char.TrackedQuests[questId] = nil
else
-- In manual-track mode, remove from tracked list
Questie.db.char.TrackedQuests[questId] = nil
-- Also remove from auto-untracked so it doesn't get re-tracked
Questie.db.char.AutoUntrackedQuests[questId] = nil
end
if Questie.db.profile.hideUntrackedQuestsMapIcons then
-- Hides objective icons for untracked quests.
@@ -2346,11 +2361,8 @@ function QuestieTracker:AQW_Insert(index, expire)
end
else
if Questie.db.char.AutoUntrackedQuests[questId] then
-- Quest was manually hidden — shift-click re-tracks it
Questie.db.char.AutoUntrackedQuests[questId] = nil
-- Add quest to the tracker
elseif IsShiftKeyDown() and QuestLogFrame:IsShown() then
Questie.db.char.AutoUntrackedQuests[questId] = true
end
end
+62 -3
View File
@@ -698,6 +698,41 @@ end
-- Intentionally NOT stored in QuestiePlayer.currentQuestlog so arrow/map/other modules
-- don't try to call DB-only methods on them.
TrackerUtils._fallbackQuests = TrackerUtils._fallbackQuests or {}
-- Reverse-lookup: given a localized zone name string, find the area ID from l10n.zoneLookup.
local function GetAreaIdByZoneName(zoneName)
if not zoneName or zoneName == "" then return 0 end
for _, zoneTable in pairs(l10n.zoneLookup) do
for areaId, name in pairs(zoneTable) do
if name == zoneName then return areaId end
end
end
return 0
end
-- Walk the quest log to find the zone header for a given questId.
-- In 3.3.5, zone names appear as isHeader=true entries above their quests.
-- Returns the header title string, or nil if not found.
local function GetQuestLogZoneName(questId)
local targetIndex = nil
local total = GetNumQuestLogEntries and GetNumQuestLogEntries() or 0
for i = 1, total do
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
if not isHeader and logId == questId then
targetIndex = i
break
end
end
if not targetIndex then return nil end
for i = targetIndex, 1, -1 do
local title, _, _, isHeader = GetQuestLogTitle(i)
if isHeader and title and title ~= "" then
return title
end
end
return nil
end
-- Returns nil if the quest is not currently in the quest log.
function TrackerUtils:BuildFallbackQuest(questId)
for i = 1, GetNumQuestLogEntries() do
@@ -723,13 +758,24 @@ function TrackerUtils:BuildFallbackQuest(questId)
end
end
local zoneId = GetCurrentMapAreaID and GetCurrentMapAreaID() or 0
-- Walk backwards from i in the quest log to find the zone header.
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
local zoneText = nil
for h = i, 1, -1 do
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
if hIsHeader and hTitle and hTitle ~= "" then
zoneText = hTitle
break
end
end
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
local quest = {
Id = questId,
name = title or ("Quest " .. questId),
level = level or 0,
zoneOrSort = zoneId,
zoneName = zoneText,
Objectives = objectives,
SpecialObjectives = {},
isFallback = true,
@@ -774,7 +820,7 @@ function TrackerUtils:GetSortedQuestIds()
local capturedId = qid
quest.IsComplete = function(self)
for i = 1, GetNumQuestLogEntries() do
local _, _, _, _, isHeader, _, isCompleteFlag, _, logId = GetQuestLogTitle(i)
local _, _, _, isHeader, _, isCompleteFlag, _, logId = GetQuestLogTitle(i)
if not isHeader and logId == capturedId then
return (isCompleteFlag == 1 or isCompleteFlag == true) and 1 or 0
end
@@ -784,10 +830,23 @@ function TrackerUtils:GetSortedQuestIds()
if not quest.Objectives then quest.Objectives = {} end
if not quest.SpecialObjectives then quest.SpecialObjectives = {} end
if not quest.ExtraObjectives then quest.ExtraObjectives = {} end
-- Use the quest log header walk (canonical 3.3.5 zone resolution)
if not quest.zoneName or quest.zoneName == "" then
local logZone = GetQuestLogZoneName(capturedId)
if logZone then
quest.zoneName = logZone
quest.zoneOrSort = GetAreaIdByZoneName(logZone) or 0
end
end
QuestiePlayer.currentQuestlog[qid] = quest
else
-- No object at all — build one from the log
local fallback = TrackerUtils._fallbackQuests[qid]
-- Re-build if cached without zone info (e.g. was built before log was ready)
if fallback and not fallback.zoneName then
TrackerUtils._fallbackQuests[qid] = nil
fallback = nil
end
if not fallback then
fallback = TrackerUtils:BuildFallbackQuest(qid)
if fallback then
@@ -807,7 +866,7 @@ function TrackerUtils:GetSortedQuestIds()
-- Create questDetails table keys and insert values
questDetails[qid] = {}
questDetails[qid].quest = quest
questDetails[qid].zoneName = _GetZoneName(quest.zoneOrSort, qid)
questDetails[qid].zoneName = quest.zoneName or _GetZoneName(quest.zoneOrSort, qid)
if quest:IsComplete() == 1 or (not next(quest.Objectives)) then
questDetails[qid].questCompletePercent = 1
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 30300
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.1.4|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.3.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.1.4
## Version: 1.3.0
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 30300
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.1.4|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.3.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.1.4
## Version: 1.3.0
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 11200
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.1.4|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.3.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.1.4
## Version: 1.3.0
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
## SavedVariables: QuestieConfig
+1 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.2.2
## Version: 1.3.0
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
## SavedVariables: QuestieConfig, QuestieLearnerDB
+77
View File
@@ -0,0 +1,77 @@
-- Tests/QuestieLearner_spec.lua
require("Tests/wow_api_mock")
describe("QuestieLearner", function()
local QuestieLearner
setup(function()
-- Mocking QuestieLoader for this test
_G.QuestieLoader.ImportModule = function(_, name)
if name == "QuestieDB" then return _G.QuestieDB end
if name == "QuestieQuest" then return _G.QuestieQuest end
if name == "QuestiePlayer" then return _G.QuestiePlayer end
if name == "QuestLogCache" then return _G.QuestLogCache end
if name == "QuestieCompat" then return _G.QuestieCompat end
return {}
end
_G.QuestieLoader.CreateModule = function(_, name)
_G[name] = {}
return _G[name]
end
-- Load the module (this assumes busted is run from project root)
package.loaded["Modules/QuestieLearner"] = nil
QuestieLearner = require("Modules/QuestieLearner")
-- Initialize
QuestieLearner:Initialize()
end)
it("should scale coordinates by 100 in OnCombatLogEvent", function()
-- Simulated combat log event info (npcId 21878)
local unitGUID = "Creature-0-1234-567-89-21878-0000000000"
local unitName = "Felboar"
-- Mock the player position to return raw decimals 0.35, 0.45
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 946, 0.35, 0.45
end
-- Trigger event
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", false, unitGUID, unitName, 0, 0, unitGUID, unitName, 0, 0)
-- The cache should store 35.0, 45.0
local cached = QuestieLearner.private.recentKills[unitGUID]
assert.is_not_nil(cached)
assert.are.equal(35.0, cached.x)
assert.are.equal(45.0, cached.y)
end)
it("should only learn spell casts that are quest objectives", function()
-- Reset data
Questie.db.global.learnedData.queries = {}
-- Case 1: Matching objective
local questId = 12345
local spellId = 29228 -- Flame Shock
_G.QuestieDB.GetQuest = function(_, id)
return {
Id = id,
Objectives = {
{ type = "spell", id = spellId }
}
}
end
_G.QuestLogCache.GetQuestID = function() return questId end
QuestieLearner:LearnSpellCast(spellId, "Flame Shock", "Enemy NPC")
-- Result: data should have a log for this quest/spell
assert.is_not_nil(Questie.db.global.learnedData.quests[questId])
assert.is_not_nil(Questie.db.global.learnedData.quests[questId][3]) -- spell node
assert.are.equal(spellId, Questie.db.global.learnedData.quests[questId][3][1])
end)
end)
+90
View File
@@ -0,0 +1,90 @@
-- Tests/wow_api_mock.lua
-- Minimal mock of World of Warcraft API for Busted unit tests
_G = _G or {}
-- Mock Globals
_G.Questie = {
DEBUG_LEARNER = "LEARNER",
DEBUG_DEVELOP = "DEVELOP",
db = {
global = {
learnedData = {
npcs = {},
quests = {},
items = {},
objects = {},
settings = {
learnQuests = true,
learnNPCs = true,
learnItems = true,
learnObjects = true,
}
}
},
profile = {
learnedData = {
settings = {
learnQuests = true,
learnNPCs = true,
}
}
}
},
Debug = function(self, level, ...)
-- print("[" .. tostring(level) .. "]", ...)
end,
Error = function(self, ...)
-- print("[ERROR]", ...)
end
}
_G.QuestieLoader = {
ImportModule = function(self, name)
if name == "QuestieDB" then return _G.QuestieDB end
if name == "QuestieQuest" then return _G.QuestieQuest end
if name == "QuestiePlayer" then return _G.QuestiePlayer end
if name == "QuestLogCache" then return _G.QuestLogCache end
if name == "QuestieLib" then return {} end
if name == "QuestieCompat" then return _G.QuestieCompat end
return {}
end,
CreateModule = function(self, name)
_G[name] = {}
return _G[name]
end
}
_G.QuestieDB = {
npcDataOverrides = {},
QueryNPCSingle = function() return nil end,
GetQuest = function() return nil end,
}
_G.QuestieCompat = {
GetCurrentPlayerPosition = function() return 1, 0.5, 0.5 end,
}
_G.QuestiePlayer = {
GetPlayerLevel = function() return 70 end,
}
_G.QuestLogCache = {
GetQuestID = function() return 123 end,
}
-- WoW Functions
_G.GetTime = function() return os.time() end
_G.time = os.time
_G.floor = math.floor
_G.UnitName = function(unit) return "TestUnit" end
_G.UnitLevel = function(unit) return 70 end
_G.UnitGUID = function(unit) return "Creature-0-1234-567-89-1000-0000000000" end
_G.GetRealZoneText = function() return "Shadowmoon Valley" end
_G.GetInstanceInfo = function() return "Shadowmoon Valley", nil, nil, nil, nil, nil, nil, 530 end
_G.C_Timer = {
After = function(duration, callback) callback() end,
}
_G.CreateFrame = function() return { RegisterEvent = function() end, SetScript = function() end } end
return _G
-49
View File
@@ -1,49 +0,0 @@
param(
[string]$InputFile,
[string]$OutputDir,
[string]$TableKey,
[int]$MaxKB = 850
)
$lines = Get-Content $InputFile
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($InputFile)
$dataStartLine = ($lines | Select-String -Pattern "^\[" | Select-Object -First 1).LineNumber - 1
$totalLines = $lines.Count
Write-Host "Processing $InputFile"
Write-Host "Data entries start at line $($dataStartLine+1) of $totalLines"
$header = "-- AUTO GENERATED FILE! DO NOT EDIT! (split chunk)`r`nif not QuestieLoader then return end`r`nlocal QuestieDB = QuestieLoader:ImportModule(`"QuestieDB`")`r`nQuestieDB.$TableKey = QuestieDB.$TableKey or {}`r`nlocal _d = QuestieDB.$TableKey`r`n"
$chunkIndex = 1
$currentLines = New-Object System.Collections.Generic.List[string]
$currentLines.Add($header)
$currentSize = [System.Text.Encoding]::UTF8.GetByteCount($header)
for ($i = $dataStartLine; $i -lt $totalLines; $i++) {
$line = $lines[$i]
if ($line -match "^\}\]\]") { break }
$converted = $line -replace "^\[(\d+)\]\s*=", "_d[`$1] ="
$lineBytes = [System.Text.Encoding]::UTF8.GetByteCount($converted + "`n")
if ($currentSize + $lineBytes -gt ($MaxKB * 1024) -and $currentLines.Count -gt 5) {
$outFile = Join-Path $OutputDir "${baseName}_${chunkIndex}.lua"
$currentLines | Set-Content $outFile
Write-Host " Wrote chunk $chunkIndex -> $outFile ($([math]::Round($currentSize/1KB))KB)"
$chunkIndex++
$currentLines = New-Object System.Collections.Generic.List[string]
$currentLines.Add($header)
$currentSize = [System.Text.Encoding]::UTF8.GetByteCount($header)
}
$currentLines.Add($converted)
$currentSize += $lineBytes
}
if ($currentLines.Count -gt 5) {
$outFile = Join-Path $OutputDir "${baseName}_${chunkIndex}.lua"
$currentLines | Set-Content $outFile
Write-Host " Wrote chunk $chunkIndex -> $outFile ($([math]::Round($currentSize/1KB))KB)"
}
Write-Host "Done. $chunkIndex chunks written."
+26
View File
@@ -176,6 +176,32 @@
</div>
<div class="container">
<h2 id="v130">v1.3.0 — QuestieLearner Confidence &amp; Tiered Pruning Engine</h2>
<p><em>Implements a robust verification model for learned data, introducing a tiered confidence system (Verified vs Unconfirmed), automated stale data cleanup with protection for high-confidence entries, and a critical correction to coordinate scaling logic for 3.3.5a combat log events. Refactors global data sharing to utilize hidden chat channels for community-wide confidence calculation.</em></p>
<h3>QuestieLearner.lua — Confidence &amp; Coordinate Scaling</h3>
<ul>
<li><strong>[Coordinate Scaling Fix — 0-100 normalization]</strong> <code>OnCombatLogEvent</code> now explicitly scales player position coordinates by 100 before recording. Previously, <code>C_Map.GetPlayerMapPosition</code> returned values on a 0-1 range, while Questie's internal modules and map pins expect 0-100. This fix resolves the "pins in the top-left corner" bug for all learned data.</li>
<li><strong>[Tiered Data Model — Verified vs Unconfirmed]</strong> Introduced the <code>mc</code> (Match Count) field as a primary confidence metric. Data is promoted to "Verified" status once <code>mc >= minConfidencePins</code> (default: 2). Verified data is exempt from automatic time-based pruning.</li>
<li><strong>[Last Seen (ls) Timestamping]</strong> Added a <code>ls</code> key to the learned data schema for NPCs, Objects, Quests, and Items. Updated on every kill, interaction, or network confirmation. This provides the temporal baseline for the new stale data cleanup engine.</li>
<li><strong>[Confidence-based Map Pin Gating]</strong> Modified the map pin generation logic to honor the <code>minConfidencePins</code> setting. Pins only appear on the map/minimap if the learned data has been confirmed by multiple kills (either local or received via network).</li>
<li><strong>[Global Data Sharing — QuestieComms Refactor]</strong> Switched from <code>PARTY</code>/<code>RAID</code> channels to a hidden global channel for learned data broadcast. This allows kills by any Questie-X user in the vicinity to contribute to local data confidence, effectively crowd-sourcing verification in real-time.</li>
<li><strong>[Tooltip Confidence Display]</strong> The tooltip handler now pulls the <code>mc</code> count for learned entries and displays it alongside the "Learned" label (e.g., <code>(Learned - Confidence: 3)</code>), providing immediate visual feedback on data reliability.</li>
</ul>
<h3>QuestieLearnerExport.lua — Tiered Pruning Engine</h3>
<ul>
<li><strong>[RunPrune — Stale threshold logic]</strong> Implemented automated time-based pruning in the <code>Cleanup</code> function. Only entries marked as "Unconfirmed" (low confidence) are subject to expiration. The expiration logic uses <code>(time() - entry.ls) > staleThreshold</code> (default: 90 days).</li>
<li><strong>[Verified Protection]</strong> Verified entries are explicitly protected from the time-based cleanup loop unless the <code>pruneVerified</code> setting is manually enabled by the user.</li>
</ul>
<h3>QuestieOptionsDatabase.lua — Verification Controls</h3>
<ul>
<li><strong>[Stale Threshold Slider]</strong> Added a configuration slider in the Database tab allowing users to set the cleanup window (1 to 180 days).</li>
<li><strong>[Prune Verified Toggle]</strong> Added a checkbox to allow manual purging of verified data if desired.</li>
<li><strong>[Prioritize My Data]</strong> Implemented a "Prioritize My Data" toggle that dynamically hides static database pins when high-confidence learned data exists for the same NPC in a zone.</li>
</ul>
<h2 id="v129">v1.2.9 — QuestieLearner Cross-Link Engine + Tracker Zone Fix + Untrack Fix</h2>
<p><em>Introduces a universal bidirectional cross-link engine in QuestieLearner that automatically stitches relationships between all four entity types (NPCs, Quests, Objects, Items) as data is learned — no manual wiring needed. Fixes the tracker's persistent "Unknown Zone" header for custom/unknown quests by replacing unreliable map API calls with the canonical 3.3.5a quest log header walk. Fixes a logic inversion in UntrackQuestId that prevented shift-click untacking from working.</em></p>