feat: release v1.3.0 - QuestieLearner Confidence & Tiered Pruning Engine
This commit is contained in:
@@ -13,3 +13,7 @@ __pycache__/
|
||||
coords.lua
|
||||
debug.lua
|
||||
debug_tooltip.lua
|
||||
tmp_*.py
|
||||
.history/
|
||||
Research/
|
||||
Tools/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+997
-72
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -176,6 +176,32 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2 id="v130">v1.3.0 — QuestieLearner Confidence & 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 & 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user