v9.7.2: Ebonhold Database integration and core logic refinements
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
---@class AvailableQuests
|
||||
local AvailableQuests = QuestieLoader:CreateModule("AvailableQuests")
|
||||
|
||||
---@type ThreadLib
|
||||
local ThreadLib = QuestieLoader:ImportModule("ThreadLib")
|
||||
---@type QuestieDB
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
---@type ZoneDB
|
||||
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
|
||||
---@type QuestiePlayer
|
||||
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
|
||||
---@type QuestieMap
|
||||
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
|
||||
---@type QuestieTooltips
|
||||
local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips")
|
||||
---@type QuestieCorrections
|
||||
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
|
||||
---@type QuestieQuestBlacklist
|
||||
local QuestieQuestBlacklist = QuestieLoader:ImportModule("QuestieQuestBlacklist")
|
||||
---@type IsleOfQuelDanas
|
||||
local IsleOfQuelDanas = QuestieLoader:ImportModule("IsleOfQuelDanas")
|
||||
---@type QuestieLib
|
||||
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
|
||||
|
||||
local GetQuestGreenRange = GetQuestGreenRange
|
||||
local yield = coroutine.yield
|
||||
local tinsert = table.insert
|
||||
local NewThread = ThreadLib.ThreadSimple
|
||||
|
||||
local QUESTS_PER_YIELD = 24
|
||||
|
||||
--- Used to keep track of the active timer for CalculateAndDrawAll
|
||||
---@type Ticker|nil
|
||||
local timer
|
||||
|
||||
-- Keep track of all available quests to unload undoable when abandoning a quest
|
||||
local availableQuests = {}
|
||||
|
||||
local dungeons = ZoneDB:GetDungeons()
|
||||
|
||||
local _CalculateAvailableQuests, _DrawChildQuests, _AddStarter, _DrawAvailableQuest, _GetQuestIcon, _GetIconScaleForAvailable, _HasProperDistanceToAlreadyAddedSpawns
|
||||
|
||||
---@param callback function | nil
|
||||
function AvailableQuests.CalculateAndDrawAll(callback)
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[AvailableQuests.CalculateAndDrawAll]")
|
||||
|
||||
--? Cancel the previously running timer to not have multiple running at the same time
|
||||
if timer then
|
||||
timer:Cancel()
|
||||
end
|
||||
timer = ThreadLib.Thread(_CalculateAvailableQuests, 0, "Error in AvailableQuests.CalculateAndDrawAll", callback)
|
||||
end
|
||||
|
||||
--Draw a single available quest, it is used by the CalculateAndDrawAll function.
|
||||
---@param quest Quest
|
||||
function AvailableQuests.DrawAvailableQuest(quest) -- prevent recursion
|
||||
--? Some quests can be started by both an NPC and a GameObject
|
||||
if not quest or not quest.Starts then
|
||||
return
|
||||
end
|
||||
|
||||
if quest.Starts["GameObject"] then
|
||||
local gameObjects = quest.Starts["GameObject"]
|
||||
for i = 1, #gameObjects do
|
||||
local objId = gameObjects[i]
|
||||
local obj = QuestieDB:GetObject(objId)
|
||||
if obj and obj.id then
|
||||
_AddStarter(obj, quest, "o_" .. obj.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if quest.Starts["NPC"] then
|
||||
local npcs = quest.Starts["NPC"]
|
||||
for i = 1, #npcs do
|
||||
local starterId = npcs[i]
|
||||
local npc = QuestieDB:GetNPC(starterId)
|
||||
if npc and npc.id then
|
||||
_AddStarter(npc, quest, "m_" .. npc.id)
|
||||
else
|
||||
-- Ascension: sometimes quest starters are GameObjects but end up in the NPC starts list
|
||||
local obj = QuestieDB:GetObject(starterId)
|
||||
if obj and obj.id then
|
||||
_AddStarter(obj, quest, "o_" .. obj.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function AvailableQuests.UnloadUndoable()
|
||||
for questId, _ in pairs(availableQuests) do
|
||||
if (not QuestieDB.IsDoable(questId)) then
|
||||
QuestieMap:UnloadQuestFrames(questId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_CalculateAvailableQuests = function()
|
||||
-- Localize the variables for speeeeed
|
||||
local debugEnabled = Questie.db.profile.debugEnabled
|
||||
|
||||
local questData = QuestieDB.QuestPointers or QuestieDB.questData
|
||||
|
||||
local playerLevel = QuestiePlayer.GetPlayerLevel()
|
||||
local minLevel = playerLevel - GetQuestGreenRange("player")
|
||||
local maxLevel = playerLevel
|
||||
|
||||
if Questie.db.profile.lowLevelStyle == Questie.LOWLEVEL_RANGE then
|
||||
minLevel = Questie.db.profile.minLevelFilter
|
||||
maxLevel = Questie.db.profile.maxLevelFilter
|
||||
elseif Questie.db.profile.lowLevelStyle == Questie.LOWLEVEL_OFFSET then
|
||||
minLevel = playerLevel - Questie.db.profile.manualLevelOffset
|
||||
end
|
||||
|
||||
local completedQuests = Questie.db.char.complete
|
||||
local showRepeatableQuests = Questie.db.profile.showRepeatableQuests
|
||||
local showDungeonQuests = Questie.db.profile.showDungeonQuests
|
||||
local showRaidQuests = Questie.db.profile.showRaidQuests
|
||||
local showPvPQuests = Questie.db.profile.showPvPQuests
|
||||
local showAQWarEffortQuests = Questie.db.profile.showAQWarEffortQuests
|
||||
|
||||
local autoBlacklist = QuestieDB.autoBlacklist
|
||||
local hiddenQuests = QuestieCorrections.hiddenQuests
|
||||
local hidden = Questie.db.char.hidden
|
||||
|
||||
local currentQuestlog = QuestiePlayer.currentQuestlog
|
||||
local currentIsleOfQuelDanasQuests = IsleOfQuelDanas.quests[Questie.db.profile.isleOfQuelDanasPhase] or {}
|
||||
local aqWarEffortQuests = QuestieQuestBlacklist.AQWarEffortQuests
|
||||
|
||||
QuestieDB.activeChildQuests = {} -- Reset here so we don't need to keep track in the quest event system
|
||||
local activeChildQuests = QuestieDB.activeChildQuests
|
||||
|
||||
-- We create a local function here to improve readability but use the localized variables above.
|
||||
-- The order of checks is important here to bring the speed to a max
|
||||
local function _DrawQuestIfAvailable(questId)
|
||||
if (autoBlacklist[questId] or -- Don't show autoBlacklist quests marked as such by IsDoable
|
||||
completedQuests[questId] or -- Don't show completed quests
|
||||
hiddenQuests[questId] or -- Don't show blacklisted quests
|
||||
hidden[questId] or -- Don't show quests hidden by the player
|
||||
activeChildQuests[questId] -- We already drew this quest in a previous loop iteration
|
||||
) then
|
||||
return
|
||||
end
|
||||
|
||||
if currentQuestlog[questId] then
|
||||
_DrawChildQuests(questId, currentQuestlog, completedQuests)
|
||||
|
||||
if QuestieDB.IsComplete(questId) ~= -1 then -- The quest in the quest log is not failed, so we don't show it as available
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if (
|
||||
((not showRepeatableQuests) and QuestieDB.IsRepeatable(questId)) or -- Don't show repeatable quests if option is disabled
|
||||
((not showPvPQuests) and QuestieDB.IsPvPQuest(questId)) or -- Don't show PvP quests if option is disabled
|
||||
((not showDungeonQuests) and QuestieDB.IsDungeonQuest(questId)) or -- Don't show dungeon quests if option is disabled
|
||||
((not showRaidQuests) and QuestieDB.IsRaidQuest(questId)) or -- Don't show raid quests if option is disabled
|
||||
((not showAQWarEffortQuests) and aqWarEffortQuests[questId]) or -- Don't show AQ War Effort quests if the option disabled
|
||||
(Questie.IsClassic and currentIsleOfQuelDanasQuests[questId]) or -- Don't show Isle of Quel'Danas quests for Era/HC/SoX
|
||||
(Questie.IsSoD and QuestieDB.IsRuneAndShouldBeHidden(questId)) -- Don't show SoD Rune quests with the option disabled
|
||||
) then
|
||||
return
|
||||
end
|
||||
|
||||
if (
|
||||
(not QuestieDB.IsLevelRequirementsFulfilled(questId, minLevel, maxLevel, playerLevel)) or
|
||||
(not QuestieDB.IsDoable(questId, debugEnabled))
|
||||
) then
|
||||
--If the quests are not within level range we want to unload them
|
||||
--(This is for when people level up or change settings etc)
|
||||
|
||||
if availableQuests[questId] then
|
||||
QuestieMap:UnloadQuestFrames(questId)
|
||||
QuestieTooltips:RemoveQuest(questId)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
availableQuests[questId] = true
|
||||
|
||||
if QuestieMap.questIdFrames[questId] then
|
||||
-- We already drew this quest so we might need to update the icon (config changed/level up)
|
||||
for _, frame in ipairs(QuestieMap:GetFramesForQuest(questId)) do
|
||||
if frame and frame.data and frame.data.QuestData then
|
||||
local newIcon = _GetQuestIcon(frame.data.QuestData)
|
||||
|
||||
if newIcon ~= frame.data.Icon then
|
||||
frame:UpdateTexture(Questie.usedIcons[newIcon])
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
_DrawAvailableQuest(questId)
|
||||
end
|
||||
|
||||
local questCount = 0
|
||||
|
||||
-- 1) Base Questie DB (compiled pointers)
|
||||
for questId in pairs(questData) do
|
||||
_DrawQuestIfAvailable(questId)
|
||||
|
||||
questCount = questCount + 1
|
||||
if questCount > QUESTS_PER_YIELD then
|
||||
questCount = 0
|
||||
yield()
|
||||
end
|
||||
end
|
||||
|
||||
-- 2) Ascension override quests (not present in QuestPointers)
|
||||
-- These are injected into QuestieDB.questDataOverrides by AscensionLoader.
|
||||
local ascensionQuestIds = QuestieDB.ascensionQuestIds or QuestieDB.questDataOverrides
|
||||
if type(ascensionQuestIds) == "table" then
|
||||
for questId in pairs(ascensionQuestIds) do
|
||||
if type(questId) == "number" then
|
||||
_DrawQuestIfAvailable(questId)
|
||||
|
||||
questCount = questCount + 1
|
||||
if questCount > QUESTS_PER_YIELD then
|
||||
questCount = 0
|
||||
yield()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Mark all child quests as active when the parent quest is in the quest log
|
||||
---@param questId number
|
||||
---@param currentQuestlog table<number, boolean>
|
||||
---@param completedQuests table<number, boolean>
|
||||
_DrawChildQuests = function(questId, currentQuestlog, completedQuests)
|
||||
local childQuests = QuestieDB.QueryQuestSingle(questId, "childQuests")
|
||||
if (not childQuests) then
|
||||
return
|
||||
end
|
||||
|
||||
for _, childQuestId in pairs(childQuests) do
|
||||
if (not completedQuests[childQuestId]) and (not currentQuestlog[childQuestId]) then
|
||||
local childQuestExclusiveTo = QuestieDB.QueryQuestSingle(childQuestId, "exclusiveTo")
|
||||
local blockedByExclusiveTo = false
|
||||
for _, exclusiveToQuestId in pairs(childQuestExclusiveTo or {}) do
|
||||
if QuestiePlayer.currentQuestlog[exclusiveToQuestId] or completedQuests[exclusiveToQuestId] then
|
||||
blockedByExclusiveTo = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if (not blockedByExclusiveTo) then
|
||||
QuestieDB.activeChildQuests[childQuestId] = true
|
||||
availableQuests[childQuestId] = true
|
||||
-- Draw them right away and skip all other irrelevant checks
|
||||
_DrawAvailableQuest(childQuestId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
_DrawAvailableQuest = function(questId)
|
||||
NewThread(function()
|
||||
local quest = QuestieDB.GetQuest(questId)
|
||||
if (not quest.tagInfoWasCached) then
|
||||
QuestieDB.GetQuestTagInfo(questId) -- cache to load in the tooltip
|
||||
|
||||
quest.tagInfoWasCached = true
|
||||
end
|
||||
|
||||
AvailableQuests.DrawAvailableQuest(quest)
|
||||
end, 0)
|
||||
end
|
||||
|
||||
---@param quest Quest
|
||||
_GetQuestIcon = function(quest)
|
||||
if Questie.IsSoD == true and QuestieDB.IsSoDRuneQuest(quest.Id) then
|
||||
return Questie.ICON_TYPE_SODRUNE
|
||||
elseif QuestieDB.IsActiveEventQuest(quest.Id) then
|
||||
return Questie.ICON_TYPE_EVENTQUEST
|
||||
end
|
||||
if QuestieDB.IsPvPQuest(quest.Id) then
|
||||
return Questie.ICON_TYPE_PVPQUEST
|
||||
end
|
||||
|
||||
-- Ascension level scaling: treat the quest level used for trivial\/difficulty logic as the effective scaled level
|
||||
local playerLevel = QuestiePlayer.GetPlayerLevel()
|
||||
local effectiveQuestLevel, effectiveRequiredLevel = QuestieLib.GetTbcLevel(quest.Id, playerLevel)
|
||||
|
||||
if effectiveRequiredLevel and effectiveRequiredLevel > playerLevel then
|
||||
return Questie.ICON_TYPE_AVAILABLE_GRAY
|
||||
end
|
||||
if quest.IsRepeatable then
|
||||
return Questie.ICON_TYPE_REPEATABLE
|
||||
end
|
||||
if QuestieLib:IsQuestTrivialScaled(quest.Id, effectiveQuestLevel) then
|
||||
return Questie.ICON_TYPE_AVAILABLE_GRAY
|
||||
end
|
||||
return Questie.ICON_TYPE_AVAILABLE
|
||||
end
|
||||
|
||||
---@param starter table Either an object or an NPC
|
||||
---@param quest Quest
|
||||
---@param tooltipKey string the tooltip key. For objects it's "o_<ID>", for NPCs it's "m_<ID>"
|
||||
_AddStarter = function(starter, quest, tooltipKey)
|
||||
if (not starter) then
|
||||
return
|
||||
end
|
||||
|
||||
QuestieTooltips:RegisterQuestStartTooltip(quest.Id, starter.name, starter.id, tooltipKey)
|
||||
|
||||
local starterIcons = {}
|
||||
local starterLocs = {}
|
||||
for zone, spawns in pairs(starter.spawns or {}) do
|
||||
local alreadyAddedSpawns = {}
|
||||
if (zone and spawns) then
|
||||
local coords
|
||||
for spawnIndex = 1, #spawns do
|
||||
coords = spawns[spawnIndex]
|
||||
if #spawns == 1 or _HasProperDistanceToAlreadyAddedSpawns(coords, alreadyAddedSpawns) then
|
||||
local data = {
|
||||
Id = quest.Id,
|
||||
Icon = _GetQuestIcon(quest),
|
||||
GetIconScale = _GetIconScaleForAvailable,
|
||||
IconScale = _GetIconScaleForAvailable(),
|
||||
Type = "available",
|
||||
QuestData = quest,
|
||||
Name = starter.name,
|
||||
IsObjectiveNote = false,
|
||||
}
|
||||
|
||||
if (coords[1] == -1 or coords[2] == -1) then
|
||||
local dungeonLocation = ZoneDB:GetDungeonLocation(zone)
|
||||
if dungeonLocation then
|
||||
for _, value in ipairs(dungeonLocation) do
|
||||
QuestieMap:DrawWorldIcon(data, value[1], value[2], value[3])
|
||||
end
|
||||
end
|
||||
else
|
||||
local icon = QuestieMap:DrawWorldIcon(data, zone, coords[1], coords[2])
|
||||
if starter.waypoints then
|
||||
-- This is only relevant for waypoint drawing
|
||||
starterIcons[zone] = icon
|
||||
if not starterLocs[zone] then
|
||||
starterLocs[zone] = { coords[1], coords[2] }
|
||||
end
|
||||
end
|
||||
tinsert(alreadyAddedSpawns, coords)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Only for NPCs since objects do not move
|
||||
if starter.waypoints then
|
||||
for zone, waypoints in pairs(starter.waypoints or {}) do
|
||||
if not dungeons[zone] and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then
|
||||
if not starterIcons[zone] then
|
||||
local data = {
|
||||
Id = quest.Id,
|
||||
Icon = _GetQuestIcon(quest),
|
||||
GetIconScale = _GetIconScaleForAvailable,
|
||||
IconScale = _GetIconScaleForAvailable(),
|
||||
Type = "available",
|
||||
QuestData = quest,
|
||||
Name = starter.name,
|
||||
IsObjectiveNote = false,
|
||||
}
|
||||
starterIcons[zone] = QuestieMap:DrawWorldIcon(data, zone, waypoints[1][1][1], waypoints[1][1][2])
|
||||
starterLocs[zone] = { waypoints[1][1][1], waypoints[1][1][2] }
|
||||
end
|
||||
QuestieMap:DrawWaypoints(starterIcons[zone], waypoints, zone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_HasProperDistanceToAlreadyAddedSpawns = function(coords, alreadyAddedSpawns)
|
||||
for _, alreadyAdded in pairs(alreadyAddedSpawns) do
|
||||
local distance = QuestieLib.GetSpawnDistance(alreadyAdded, coords)
|
||||
-- 29 seems like a good distance. The "Undying Laborer" in Westfall shows both spawns for the "Horn of Lordaeron" rune
|
||||
if distance < 29 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
_GetIconScaleForAvailable = function()
|
||||
return Questie.db.profile.availableScale or 1.3
|
||||
end
|
||||
|
||||
-- Periodic cleanup to ensure completed quest icons are removed
|
||||
-- This is needed because sometimes QuestieMap:UnloadQuestFrames doesn't fully clean up
|
||||
-- or icons are redrawn by race conditions
|
||||
local cleanupTimer
|
||||
local function StartPeriodicCleanup()
|
||||
if cleanupTimer then
|
||||
cleanupTimer:Cancel()
|
||||
end
|
||||
|
||||
-- Check every 5 seconds
|
||||
cleanupTimer = C_Timer.NewTicker(5, function()
|
||||
-- Only run if Questie isn't busy
|
||||
if QuestieMap._mapDrawQueue and #QuestieMap._mapDrawQueue == 0 and
|
||||
QuestieMap._minimapDrawQueue and #QuestieMap._minimapDrawQueue == 0 then
|
||||
|
||||
local completedQuests = Questie.db.char.complete
|
||||
if not completedQuests then return end
|
||||
|
||||
for questId, frameList in pairs(QuestieMap.questIdFrames) do
|
||||
if completedQuests[questId] then
|
||||
-- This quest is complete but still has frames on the map
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[AvailableQuests] Cleanup: Removing lingering frames for completed quest:", questId)
|
||||
QuestieMap:UnloadQuestFrames(questId)
|
||||
QuestieTooltips:RemoveQuest(questId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Start the cleanup timer
|
||||
StartPeriodicCleanup()
|
||||
@@ -0,0 +1,213 @@
|
||||
---@class DailyQuests
|
||||
local DailyQuests = QuestieLoader:CreateModule("DailyQuests");
|
||||
local _DailyQuests = {}
|
||||
|
||||
--- COMPATIBILITY ---
|
||||
local IsQuestFlaggedCompleted = QuestieCompat.IsQuestFlaggedCompleted or C_QuestLog.IsQuestFlaggedCompleted
|
||||
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
|
||||
|
||||
---@type QuestieMap
|
||||
local QuestieMap = QuestieLoader:ImportModule("QuestieMap");
|
||||
---@type QuestieQuest
|
||||
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest");
|
||||
---@type QuestieTooltips
|
||||
local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips");
|
||||
---@type QuestiePlayer
|
||||
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer");
|
||||
|
||||
local nhcDailyIds, hcDailyIds, cookingDailyIds, fishingDailyIds, pvpDailyIds
|
||||
|
||||
local lastCheck
|
||||
|
||||
---@param message string
|
||||
---@return nil
|
||||
function DailyQuests:FilterDailies(message, _, _)
|
||||
if message and Questie.db.profile.showRepeatableQuests and QuestiePlayer.GetPlayerLevel() == 70 then
|
||||
-- If the REPUTABLE message is empty, i.e contains "::::::::::" we don't count it as a check.
|
||||
if (not lastCheck) and not string.find(message, "::::::::::") then
|
||||
lastCheck = GetTime();
|
||||
elseif lastCheck and GetTime() - lastCheck < 10 and not string.find(message, "::::::::::") then
|
||||
lastCheck = GetTime();
|
||||
return;
|
||||
end
|
||||
|
||||
local nhcQuestId, hcQuestId, cookingQuestId, fishingQuestId, pvpQuestId = _DailyQuests:GetDailyIds(message);
|
||||
|
||||
local somethingChanged = _DailyQuests:ResetIfRequired(nhcQuestId, hcQuestId, cookingQuestId, fishingQuestId, pvpQuestId);
|
||||
if (not somethingChanged) then
|
||||
-- We are already showing the correct quests
|
||||
return;
|
||||
end
|
||||
|
||||
_DailyQuests:HandleDailyQuests(nhcDailyIds, nhcQuestId, "nhc");
|
||||
_DailyQuests:HandleDailyQuests(hcDailyIds, hcQuestId, "hc");
|
||||
_DailyQuests:HandleDailyQuests(cookingDailyIds, cookingQuestId, "cooking");
|
||||
_DailyQuests:HandleDailyQuests(fishingDailyIds, fishingQuestId, "fishing");
|
||||
_DailyQuests:HandleDailyQuests(pvpDailyIds, pvpQuestId, "pvp");
|
||||
end
|
||||
end
|
||||
|
||||
-- /run DailyQuests:FilterDailies("0:0:11364:0:11354:0:11377:0:11667:0:11340:0")
|
||||
-- /run Questie.db.char.hiddenDailies = {nhc={},hc={},cooking={},fishing={},pvp={}}
|
||||
|
||||
---@param message string
|
||||
---@return number, number, number, number, number
|
||||
function _DailyQuests:GetDailyIds(message)
|
||||
-- Each questId is followed by the timestamp from GetQuestResetTime(). We don't use that timestamp (yet)
|
||||
local _, _, nhcQuestId, _, hcQuestId, _, cookingQuestId, _, fishingQuestId, _, pvpQuestId, _ = strsplit(":", message);
|
||||
|
||||
return tonumber(nhcQuestId) or 0,
|
||||
tonumber(hcQuestId) or 0,
|
||||
tonumber(cookingQuestId) or 0,
|
||||
tonumber(fishingQuestId) or 0,
|
||||
tonumber(pvpQuestId) or 0;
|
||||
end
|
||||
|
||||
---@param nhcQuestId number
|
||||
---@param hcQuestId number
|
||||
---@param cookingQuestId number
|
||||
---@param fishingQuestId number
|
||||
---@param pvpQuestId number
|
||||
---@return boolean
|
||||
function _DailyQuests:ResetIfRequired(nhcQuestId, hcQuestId, cookingQuestId, fishingQuestId, pvpQuestId)
|
||||
local somethingChanged = false
|
||||
if nhcQuestId > 0 and (Questie.db.char.hiddenDailies.nhc[nhcQuestId] or (not next(Questie.db.char.hiddenDailies.nhc))) and (not IsQuestFlaggedCompleted(nhcQuestId)) then
|
||||
Questie.db.char.hiddenDailies.nhc = {};
|
||||
somethingChanged = true;
|
||||
end
|
||||
if hcQuestId > 0 and (Questie.db.char.hiddenDailies.hc[hcQuestId] or (not next(Questie.db.char.hiddenDailies.hc))) and (not IsQuestFlaggedCompleted(hcQuestId)) then
|
||||
Questie.db.char.hiddenDailies.hc = {};
|
||||
somethingChanged = true;
|
||||
end
|
||||
if cookingQuestId > 0 and (Questie.db.char.hiddenDailies.cooking[cookingQuestId] or (not next(Questie.db.char.hiddenDailies.cooking))) and (not IsQuestFlaggedCompleted(cookingQuestId)) then
|
||||
Questie.db.char.hiddenDailies.cooking = {};
|
||||
somethingChanged = true;
|
||||
end
|
||||
if fishingQuestId > 0 and (Questie.db.char.hiddenDailies.fishing[fishingQuestId] or (not next(Questie.db.char.hiddenDailies.fishing))) and (not IsQuestFlaggedCompleted(fishingQuestId)) then
|
||||
Questie.db.char.hiddenDailies.fishing = {};
|
||||
somethingChanged = true;
|
||||
end
|
||||
if pvpQuestId > 0 and (Questie.db.char.hiddenDailies.pvp[pvpQuestId] or (not next(Questie.db.char.hiddenDailies.pvp))) and (not IsQuestFlaggedCompleted(pvpQuestId)) then
|
||||
Questie.db.char.hiddenDailies.pvp = {};
|
||||
somethingChanged = true;
|
||||
end
|
||||
|
||||
return somethingChanged;
|
||||
end
|
||||
|
||||
---@param possibleQuestIds table<number, number>
|
||||
---@param currentQuestId number
|
||||
---@param type string
|
||||
---@return nil
|
||||
function _DailyQuests:HandleDailyQuests(possibleQuestIds, currentQuestId, type)
|
||||
if currentQuestId == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
for questId, _ in pairs(possibleQuestIds) do
|
||||
if questId == currentQuestId then
|
||||
_DailyQuests.ShowDailyQuest(questId);
|
||||
Questie.db.char.hiddenDailies[type][questId] = nil;
|
||||
else
|
||||
-- If the quest is not in the questlog remove all frames
|
||||
if (GetQuestLogIndexByID(questId) == 0) then
|
||||
_DailyQuests:HideDailyQuest(questId);
|
||||
end
|
||||
Questie.db.char.hiddenDailies[type][questId] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return nil
|
||||
function _DailyQuests:HideDailyQuest(questId)
|
||||
QuestieMap:UnloadQuestFrames(questId);
|
||||
QuestieTooltips:RemoveQuest(questId);
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return nil
|
||||
function _DailyQuests.ShowDailyQuest(questId)
|
||||
if (not QuestieMap.questIdFrames[questId]) then
|
||||
QuestieQuest.DrawDailyQuest(questId);
|
||||
end
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return boolean
|
||||
function DailyQuests:IsActiveDailyQuest(questId)
|
||||
return true
|
||||
-- TODO: This might be reusable when reworking this module
|
||||
--local hiddenQuests = Questie.db.char.hiddenDailies
|
||||
--return not (hiddenQuests.nhc[questId] or
|
||||
-- hiddenQuests.hc[questId] or
|
||||
-- hiddenQuests.cooking[questId] or
|
||||
-- hiddenQuests.fishing[questId] or
|
||||
-- hiddenQuests.pvp[questId]);
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return boolean
|
||||
function DailyQuests:IsDailyQuest(questId)
|
||||
return nhcDailyIds[questId] ~= nil or
|
||||
hcDailyIds[questId] ~= nil or
|
||||
cookingDailyIds[questId] ~= nil or
|
||||
fishingDailyIds[questId] ~= nil or
|
||||
pvpDailyIds[questId] ~= nil;
|
||||
end
|
||||
|
||||
nhcDailyIds = {
|
||||
[11364] = true,
|
||||
[11371] = true,
|
||||
[11376] = true,
|
||||
[11383] = true,
|
||||
[11385] = true,
|
||||
[11387] = true,
|
||||
[11389] = true,
|
||||
[11500] = true,
|
||||
};
|
||||
|
||||
hcDailyIds = {
|
||||
[11354] = true,
|
||||
[11362] = true,
|
||||
[11363] = true,
|
||||
[11368] = true,
|
||||
[11369] = true,
|
||||
[11370] = true,
|
||||
[11372] = true,
|
||||
[11373] = true,
|
||||
[11374] = true,
|
||||
[11375] = true,
|
||||
[11378] = true,
|
||||
[11382] = true,
|
||||
[11384] = true,
|
||||
[11386] = true,
|
||||
[11388] = true,
|
||||
[11499] = true,
|
||||
};
|
||||
|
||||
cookingDailyIds = {
|
||||
[11377] = true,
|
||||
[11379] = true,
|
||||
[11380] = true,
|
||||
[11381] = true,
|
||||
};
|
||||
|
||||
fishingDailyIds = {
|
||||
[11667] = true,
|
||||
[11665] = true,
|
||||
[11666] = true,
|
||||
[11668] = true,
|
||||
[11669] = true,
|
||||
};
|
||||
|
||||
pvpDailyIds = {
|
||||
[11335] = true,
|
||||
[11336] = true,
|
||||
[11337] = true,
|
||||
[11338] = true,
|
||||
[11339] = true,
|
||||
[11340] = true,
|
||||
[11341] = true,
|
||||
[11342] = true,
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
---@class IsleOfQuelDanas
|
||||
local IsleOfQuelDanas = QuestieLoader:CreateModule("IsleOfQuelDanas")
|
||||
---@type l10n
|
||||
local l10n = QuestieLoader:ImportModule("l10n")
|
||||
|
||||
|
||||
IsleOfQuelDanas.MAX_ISLE_OF_QUEL_DANAS_PHASES = 9
|
||||
IsleOfQuelDanas.localizedPhaseNames = {}
|
||||
|
||||
function IsleOfQuelDanas.Initialize()
|
||||
IsleOfQuelDanas.localizedPhaseNames = {
|
||||
l10n("Phase 1 - Sun's Reach Sanctum"),
|
||||
l10n("Phase 2 - Activating the Sunwell Portal"),
|
||||
l10n("Phase 2.1 - Sun's Reach Armory"),
|
||||
l10n("Phase 3 - Rebuilding the Anvil and Forge"),
|
||||
l10n("Phase 3.1 - Sun's Reach Harbor"),
|
||||
l10n("Phase 4 - Creating the Alchemy Lab"),
|
||||
l10n("Phase 4.1 - Building the Monument to the Fallen"),
|
||||
l10n("Phase 4.2 - Sun's Reach"),
|
||||
l10n("Phase 5"),
|
||||
}
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return boolean
|
||||
function IsleOfQuelDanas.CheckForActivePhase(questId)
|
||||
local isleQuests = IsleOfQuelDanas.quests
|
||||
if isleQuests[1][questId] and isleQuests[Questie.db.global.isleOfQuelDanasPhase][questId] then
|
||||
-- The accepted quest is one from the Isle Of Quel'Danas
|
||||
local phaseToSwitchTo = 2
|
||||
for i = 2, IsleOfQuelDanas.MAX_ISLE_OF_QUEL_DANAS_PHASES do
|
||||
if (not isleQuests[i][questId]) then
|
||||
-- This is the phase that unlocked this quest
|
||||
phaseToSwitchTo = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
Questie:Print(l10n("You picked up a quest from '%s'. Automatically switching to this phase...", IsleOfQuelDanas.localizedPhaseNames[phaseToSwitchTo]))
|
||||
Questie.db.global.isleOfQuelDanasPhase = phaseToSwitchTo
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- These quests are the blacklisted ones for each phase
|
||||
IsleOfQuelDanas.quests = {
|
||||
{ -- Phase 1
|
||||
[11513] = true,
|
||||
[11514] = true,
|
||||
[11520] = true,
|
||||
[11521] = true,
|
||||
[11523] = true,
|
||||
[11525] = true,
|
||||
[11526] = true,
|
||||
[11532] = true,
|
||||
[11533] = true,
|
||||
[11534] = true,
|
||||
[11535] = true,
|
||||
[11536] = true,
|
||||
[11537] = true,
|
||||
[11538] = true,
|
||||
[11539] = true,
|
||||
[11540] = true,
|
||||
[11541] = true,
|
||||
[11542] = true,
|
||||
[11543] = true,
|
||||
[11544] = true,
|
||||
[11545] = true,
|
||||
[11546] = true,
|
||||
[11547] = true,
|
||||
[11548] = true,
|
||||
[11549] = true,
|
||||
},
|
||||
{ -- Phase 2
|
||||
-- temp quests from previous phase
|
||||
[11496] = true,
|
||||
[11514] = true,
|
||||
[11524] = true,
|
||||
[11534] = true,
|
||||
--
|
||||
[11520] = true,
|
||||
[11521] = true,
|
||||
[11533] = true,
|
||||
[11535] = true,
|
||||
[11536] = true,
|
||||
[11537] = true,
|
||||
[11539] = true,
|
||||
[11540] = true,
|
||||
[11541] = true,
|
||||
[11542] = true,
|
||||
[11543] = true,
|
||||
[11544] = true,
|
||||
[11545] = true,
|
||||
[11546] = true,
|
||||
[11547] = true,
|
||||
[11548] = true,
|
||||
[11549] = true,
|
||||
},
|
||||
{ -- Phase 2.1 Shatt Portal done
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11524] = true,
|
||||
--
|
||||
[11520] = true,
|
||||
[11521] = true,
|
||||
[11533] = true,
|
||||
[11535] = true,
|
||||
[11536] = true,
|
||||
[11537] = true,
|
||||
[11539] = true,
|
||||
[11540] = true,
|
||||
[11541] = true,
|
||||
[11542] = true,
|
||||
[11543] = true,
|
||||
[11544] = true,
|
||||
[11545] = true,
|
||||
[11546] = true,
|
||||
[11548] = true,
|
||||
[11549] = true,
|
||||
},
|
||||
{ -- Phase 3
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11538] = true,
|
||||
--
|
||||
[11520] = true,
|
||||
[11521] = true,
|
||||
[11536] = true,
|
||||
[11540] = true,
|
||||
[11541] = true,
|
||||
[11543] = true,
|
||||
[11544] = true,
|
||||
[11545] = true,
|
||||
[11546] = true,
|
||||
[11548] = true,
|
||||
[11549] = true,
|
||||
},
|
||||
{ -- Phase 3.1 Anvil and Forge done
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11535] = true,
|
||||
[11538] = true,
|
||||
--
|
||||
[11520] = true,
|
||||
[11521] = true,
|
||||
[11540] = true,
|
||||
[11541] = true,
|
||||
[11543] = true,
|
||||
[11545] = true,
|
||||
[11546] = true,
|
||||
[11548] = true,
|
||||
[11549] = true,
|
||||
},
|
||||
{ -- Phase 4
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11535] = true,
|
||||
[11538] = true,
|
||||
[11539] = true,
|
||||
[11542] = true,
|
||||
--
|
||||
[11521] = true,
|
||||
[11546] = true,
|
||||
[11548] = true,
|
||||
},
|
||||
{ -- Phase 4.1 - Alchemy Lab done
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11520] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11535] = true,
|
||||
[11538] = true,
|
||||
[11539] = true,
|
||||
[11542] = true,
|
||||
--
|
||||
[11548] = true,
|
||||
},
|
||||
{ -- Phase 4.2 - Monument of the Fallen done
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11535] = true,
|
||||
[11538] = true,
|
||||
[11539] = true,
|
||||
[11542] = true,
|
||||
[11545] = true,
|
||||
--
|
||||
[11521] = true,
|
||||
[11546] = true,
|
||||
},
|
||||
{ -- Phase 5 - Both buildings done
|
||||
-- temp quests from previous phases
|
||||
[11496] = true,
|
||||
[11513] = true,
|
||||
[11517] = true,
|
||||
[11520] = true,
|
||||
[11524] = true,
|
||||
[11532] = true,
|
||||
[11535] = true,
|
||||
[11538] = true,
|
||||
[11539] = true,
|
||||
[11542] = true,
|
||||
[11545] = true,
|
||||
--
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
---@class QuestEventHandler
|
||||
local QuestEventHandler = QuestieLoader:CreateModule("QuestEventHandler")
|
||||
---@class QuestEventHandlerPrivate
|
||||
local _QuestEventHandler = QuestEventHandler.private
|
||||
|
||||
local _QuestLogUpdateQueue = {} -- Helper module
|
||||
local questLogUpdateQueue = {} -- The actual queue
|
||||
|
||||
---@type QuestEventHandlerPrivate
|
||||
QuestEventHandler.private = QuestEventHandler.private or {}
|
||||
---@type QuestLogCache
|
||||
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache")
|
||||
---@type QuestieQuest
|
||||
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
|
||||
---@type QuestieJourney
|
||||
local QuestieJourney = QuestieLoader:ImportModule("QuestieJourney")
|
||||
---@type QuestieNameplate
|
||||
local QuestieNameplate = QuestieLoader:ImportModule("QuestieNameplate")
|
||||
---@type QuestieLib
|
||||
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
|
||||
---@type QuestieDB
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
---@type QuestieAnnounce
|
||||
local QuestieAnnounce = QuestieLoader:ImportModule("QuestieAnnounce")
|
||||
---@type IsleOfQuelDanas
|
||||
local IsleOfQuelDanas = QuestieLoader:ImportModule("IsleOfQuelDanas")
|
||||
---@type QuestieCombatQueue
|
||||
local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue")
|
||||
---@type QuestieTracker
|
||||
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
|
||||
---@type l10n
|
||||
local l10n = QuestieLoader:ImportModule("l10n")
|
||||
|
||||
--- COMPATIBILITY ---
|
||||
local C_Timer = QuestieCompat.C_Timer
|
||||
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
||||
local GetItemInfo = QuestieCompat.GetItemInfo
|
||||
|
||||
local tableRemove = table.remove
|
||||
|
||||
local QUEST_LOG_STATES = {
|
||||
QUEST_ACCEPTED = "QUEST_ACCEPTED",
|
||||
QUEST_TURNED_IN = "QUEST_TURNED_IN",
|
||||
QUEST_REMOVED = "QUEST_REMOVED",
|
||||
QUEST_ABANDONED = "QUEST_ABANDONED"
|
||||
}
|
||||
|
||||
local eventFrame = CreateFrame("Frame", "QuestieQuestEventFrame")
|
||||
local questLog = {}
|
||||
local questLogUpdateQueueSize = 1
|
||||
local skipNextUQLCEvent = false
|
||||
local doFullQuestLogScan = false
|
||||
local deletedQuestItem = false
|
||||
|
||||
--- Registers all events that are required for questing (accepting, removing, objective updates, ...)
|
||||
function QuestEventHandler:RegisterEvents()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] RegisterEvents")
|
||||
eventFrame:RegisterEvent("QUEST_ACCEPTED")
|
||||
eventFrame:RegisterEvent("QUEST_TURNED_IN")
|
||||
eventFrame:RegisterEvent("QUEST_REMOVED")
|
||||
eventFrame:RegisterEvent("QUEST_LOG_UPDATE")
|
||||
eventFrame:RegisterEvent("QUEST_WATCH_UPDATE")
|
||||
eventFrame:RegisterEvent("UNIT_QUEST_LOG_CHANGED")
|
||||
eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
||||
eventFrame:RegisterEvent("NEW_RECIPE_LEARNED") -- Spell objectives; Runes in SoD count as recipes because "Engraving" is a profession?
|
||||
--eventFrame:RegisterEvent("SPELLS_CHANGED") -- Spell objectives
|
||||
|
||||
eventFrame:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE")
|
||||
|
||||
eventFrame:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
|
||||
eventFrame:SetScript("OnEvent", _QuestEventHandler.OnEvent)
|
||||
|
||||
-- StaticPopup dialog hooks. Deleteing Quest items do not always trigger a Quest Log Update.
|
||||
hooksecurefunc("StaticPopup_Show", function(...)
|
||||
-- Hook StaticPopup_Show. If we find the "DELETE_ITEM" dialog, check for Quest Items and notify the player.
|
||||
local which, text_arg1 = ...
|
||||
if which == "DELETE_ITEM" then
|
||||
local quest
|
||||
local questName
|
||||
local foundQuestItem = false
|
||||
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] StaticPopup_Show: Item Name: ", text_arg1)
|
||||
|
||||
if deletedQuestItem == true then
|
||||
deletedQuestItem = false
|
||||
end
|
||||
|
||||
for questLogIndex = 1, 75 do
|
||||
local title, _, _, isHeader, _, _, _, questId = GetQuestLogTitle(questLogIndex)
|
||||
|
||||
if (not title) then
|
||||
break
|
||||
end
|
||||
|
||||
if (not isHeader) then
|
||||
quest = QuestieDB.GetQuest(questId)
|
||||
|
||||
if quest then
|
||||
local info = StaticPopupDialogs[which]
|
||||
local sourceItemId, soureItemName, sourceItemType, soureClassID
|
||||
local reqSourceItemId, reqSoureItemName, reqSourceItemType, reqSoureClassID
|
||||
|
||||
if quest.sourceItemId then
|
||||
sourceItemId = quest.sourceItemId
|
||||
|
||||
if sourceItemId then
|
||||
soureItemName, _, _, _, _, sourceItemType, _, _, _, _, _, soureClassID = GetItemInfo(sourceItemId)
|
||||
end
|
||||
end
|
||||
|
||||
if quest.requiredSourceItems then
|
||||
reqSourceItemId = quest.requiredSourceItems[1]
|
||||
|
||||
if reqSourceItemId then
|
||||
reqSoureItemName, _, _, _, _, reqSourceItemType, _, _, _, _, _, reqSoureClassID = GetItemInfo(reqSourceItemId)
|
||||
end
|
||||
end
|
||||
|
||||
if sourceItemId and soureItemName and sourceItemType and soureClassID and (sourceItemType == "Quest" or soureClassID == 12) and QuestieDB.QueryItemSingle(sourceItemId, "class") == 12 and text_arg1 == soureItemName then
|
||||
questName = quest.name
|
||||
foundQuestItem = true
|
||||
break
|
||||
elseif reqSourceItemId and reqSoureItemName and reqSourceItemType and reqSoureClassID and (reqSourceItemType == "Quest" or reqSoureClassID == 12) and QuestieDB.QueryItemSingle(reqSourceItemId, "class") == 12 and text_arg1 == reqSoureItemName then
|
||||
questName = quest.name
|
||||
foundQuestItem = true
|
||||
break
|
||||
else
|
||||
if quest.Objectives and #quest.Objectives > 0 then
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
if text_arg1 == objective.Description then
|
||||
questName = quest.name
|
||||
foundQuestItem = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if foundQuestItem and quest and questName then
|
||||
local frame, text
|
||||
|
||||
for i = 1, STATICPOPUP_NUMDIALOGS do
|
||||
frame = _G["StaticPopup" .. i]
|
||||
if (frame:IsShown()) and ((frame.text.text_arg1 == text_arg1) or (string.find(frame.text:GetText(), text_arg1))) then
|
||||
text = _G[frame:GetName() .. "Text"]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if frame ~= nil and text ~= nil then
|
||||
local updateText = l10n("Quest Item %%s might be needed for the quest %%s. \n\nAre you sure you want to delete this?")
|
||||
text:SetFormattedText(updateText, text_arg1, questName)
|
||||
text.text_arg1 = updateText
|
||||
|
||||
StaticPopup_Resize(frame, which)
|
||||
deletedQuestItem = true
|
||||
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] StaticPopup_Show: Quest Item Detected. Updating Static Popup.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
hooksecurefunc("DeleteCursorItem", function()
|
||||
-- Hook DeleteCursorItem so we know when the player clicks the Accept button
|
||||
if deletedQuestItem then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] DeleteCursorItem: Quest Item deleted. Update all quests.")
|
||||
|
||||
C_Timer.After(0.25, function()
|
||||
_QuestEventHandler:UpdateAllQuests()
|
||||
deletedQuestItem = false
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
_QuestEventHandler:InitQuestLog()
|
||||
end
|
||||
|
||||
--- On Login mark all quests in the quest log with QUEST_ACCEPTED state
|
||||
function _QuestEventHandler:InitQuestLog()
|
||||
-- Fill the QuestLogCache for first time
|
||||
local cacheMiss, changes = QuestLogCache.CheckForChanges(nil)
|
||||
-- if cacheMiss then
|
||||
-- TODO actually can happen in rare edge case if player accepts new quest during questie init. *cough*
|
||||
-- or if someone managed to overflow game cache already at this point.
|
||||
--Questie:Error("Did you accept a quest during InitQuestLog? Please report on Github or Discord. Game's quest log cache is not ok. This shouldn't happen. Questie may malfunction.")
|
||||
-- end
|
||||
|
||||
for questId, _ in pairs(changes) do
|
||||
questLog[questId] = {
|
||||
state = QUEST_LOG_STATES.QUEST_ACCEPTED
|
||||
}
|
||||
QuestieLib:CacheItemNames(questId)
|
||||
end
|
||||
end
|
||||
|
||||
--- Fires when a quest is accepted in anyway.
|
||||
---@param questLogIndex number
|
||||
---@param questId number
|
||||
function _QuestEventHandler:QuestAccepted(questLogIndex, questId)
|
||||
questId = questId or select(8, GetQuestLogTitle(questLogIndex))
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_ACCEPTED", questLogIndex, questId)
|
||||
|
||||
if questLog[questId] and questLog[questId].timer then
|
||||
-- We had a QUEST_REMOVED event which started this timer and now it was accepted again.
|
||||
-- So the quest was abandoned before, because QUEST_TURNED_IN would have run before QUEST_ACCEPTED.
|
||||
questLog[questId].timer:Cancel()
|
||||
questLog[questId].timer = nil
|
||||
QuestieCombatQueue:Queue(function()
|
||||
_QuestEventHandler:MarkQuestAsAbandoned(questId)
|
||||
end)
|
||||
end
|
||||
|
||||
questLog[questId] = {}
|
||||
|
||||
-- Timed quests do not need a full Quest Log Update.
|
||||
-- TODO: Add achievement timers later.
|
||||
local questTimers = GetQuestTimers(questId)
|
||||
if type(questTimers) == "number" then
|
||||
skipNextUQLCEvent = false
|
||||
else
|
||||
skipNextUQLCEvent = true
|
||||
end
|
||||
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieLib:CacheItemNames(questId)
|
||||
_QuestEventHandler:HandleQuestAccepted(questId)
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_ACCEPTED - skipNextUQLCEvent - ", skipNextUQLCEvent)
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
---@return boolean true @if the function was successful, false otherwise
|
||||
function _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
local idx = QuestieCompat.GetQuestLogIndexByID(questId)
|
||||
if not idx then
|
||||
_QuestLogUpdateQueue:Insert(function()
|
||||
return _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
end)
|
||||
return false
|
||||
end
|
||||
-- We first check the quest objectives and retry in the next QLU event if they are not correct yet
|
||||
local cacheMiss, changes = QuestLogCache.CheckForChanges({ [questId] = true })
|
||||
if cacheMiss then
|
||||
-- if cacheMiss, no need to check changes as only 1 questId
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Objectives are not cached yet")
|
||||
_QuestLogUpdateQueue:Insert(function()
|
||||
return _QuestEventHandler:HandleQuestAccepted(questId)
|
||||
end)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Objectives are correct. Calling accept logic. quest:", questId)
|
||||
questLog[questId].state = QUEST_LOG_STATES.QUEST_ACCEPTED
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
QuestieJourney:AcceptQuest(questId)
|
||||
QuestieAnnounce:AcceptedQuest(questId)
|
||||
|
||||
local isLastIslePhase = Questie.db.global.isleOfQuelDanasPhase == IsleOfQuelDanas.MAX_ISLE_OF_QUEL_DANAS_PHASES
|
||||
if Questie.IsWotlk and (not isLastIslePhase) and IsleOfQuelDanas.CheckForActivePhase(questId) then
|
||||
QuestieQuest:SmoothReset()
|
||||
else
|
||||
QuestieQuest:AcceptQuest(questId)
|
||||
end
|
||||
QuestieCompat.C_Timer.After(0.2, function()
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Fires when a quest is turned in
|
||||
---@param questId number
|
||||
---@param xpReward number
|
||||
---@param moneyReward number
|
||||
function _QuestEventHandler:QuestTurnedIn(questId, xpReward, moneyReward)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_TURNED_IN", xpReward, moneyReward, questId)
|
||||
|
||||
if questLog[questId] and questLog[questId].timer then
|
||||
-- Cancel the timer so the quest is not marked as abandoned
|
||||
questLog[questId].timer:Cancel()
|
||||
questLog[questId].timer = nil
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "was turned in and is completed")
|
||||
|
||||
if questLog[questId] then
|
||||
-- There are quests which you just turn in so there is no preceding QUEST_ACCEPTED event and questLog[questId]
|
||||
-- is empty
|
||||
questLog[questId].state = QUEST_LOG_STATES.QUEST_TURNED_IN
|
||||
elseif QuestieCompat.Is335 then
|
||||
questLog[questId] = {state = QUEST_LOG_STATES.QUEST_TURNED_IN}
|
||||
end
|
||||
|
||||
local parentQuest = QuestieDB.QueryQuestSingle(questId, "parentQuest")
|
||||
|
||||
if parentQuest and parentQuest > 0 then
|
||||
-- Quests like "The Warsong Reports" have child quests which are just turned in. These child quests only
|
||||
-- fire QUEST_TURNED_IN + QUEST_LOG_UPDATE
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "Quest:", questId, "Has a Parent Quest - do a full Quest Log check")
|
||||
doFullQuestLogScan = true
|
||||
end
|
||||
|
||||
local itemName, _, _, quality, _, itemID = GetQuestLogRewardInfo(GetNumQuestLogRewards(questId), questId)
|
||||
|
||||
if (itemID ~= nil or itemName ~= nil) and quality == 1 then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "Quest:", questId, "Recieved a possible Quest Item - do a full Quest Log check")
|
||||
doFullQuestLogScan = true
|
||||
skipNextUQLCEvent = false
|
||||
else
|
||||
skipNextUQLCEvent = true
|
||||
end
|
||||
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:SetObjectivesDirty(questId) -- is this necessary? should whole quest.Objectives be cleared at some point of quest removal?
|
||||
|
||||
-- Don't immediately mark as complete, wait for QUEST_REMOVED to confirm it was actually turned in
|
||||
-- This prevents completed quests from being saved as turned in when abandoned
|
||||
-- QuestieQuest:CompleteQuest(questId)
|
||||
-- QuestieJourney:CompleteQuest(questId)
|
||||
-- QuestieAnnounce:CompletedQuest(questId)
|
||||
-- questLog[questId] = nil
|
||||
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Fires when a quest is removed from the quest log. This includes turning it in and abandoning it.
|
||||
---@param questId number
|
||||
function _QuestEventHandler:QuestRemoved(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_REMOVED", questId)
|
||||
doFullQuestLogScan = false
|
||||
|
||||
if (not questLog[questId]) then
|
||||
questLog[questId] = {}
|
||||
end
|
||||
|
||||
-- The party members don't care whether a quest was turned in or abandoned, so we can just broadcast here
|
||||
Questie:SendMessage("QC_ID_BROADCAST_QUEST_REMOVE", questId)
|
||||
|
||||
-- QUEST_TURNED_IN was called before QUEST_REMOVED --> quest was turned in
|
||||
if questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "was turned in before. Completing quest.")
|
||||
|
||||
-- Now that we confirmed the quest was actually turned in (not abandoned), mark it as complete
|
||||
QuestieQuest:CompleteQuest(questId)
|
||||
QuestieJourney:CompleteQuest(questId)
|
||||
QuestieAnnounce:CompletedQuest(questId)
|
||||
|
||||
questLog[questId] = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- QUEST_REMOVED can fire before QUEST_TURNED_IN. If QUEST_TURNED_IN is not called after X seconds the quest
|
||||
-- was abandoned
|
||||
questLog[questId] = {
|
||||
state = QUEST_LOG_STATES.QUEST_REMOVED,
|
||||
timer = C_Timer.NewTicker(1, function()
|
||||
_QuestEventHandler:MarkQuestAsAbandoned(questId)
|
||||
end, 1)
|
||||
}
|
||||
skipNextUQLCEvent = true
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_REMOVED - skipNextUQLCEvent - ", skipNextUQLCEvent)
|
||||
end
|
||||
|
||||
---@param questId number
|
||||
function _QuestEventHandler:MarkQuestAsAbandoned(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "QuestEventHandler:MarkQuestAsAbandoned")
|
||||
local questEntry = questLog[questId]
|
||||
|
||||
-- so we don't attempt to index a nil value.
|
||||
if (not questEntry) then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "QuestEventHandler:MarkQuestAsAbandoned - questLog entry missing for", questId)
|
||||
return
|
||||
end
|
||||
|
||||
if questEntry.state == QUEST_LOG_STATES.QUEST_REMOVED then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "was abandoned")
|
||||
questEntry.state = QUEST_LOG_STATES.QUEST_ABANDONED
|
||||
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:SetObjectivesDirty(questId) -- is this necessary? should whole quest.Objectives be cleared at some point of quest removal?
|
||||
|
||||
QuestieQuest:AbandonedQuest(questId)
|
||||
QuestieJourney:AbandonQuest(questId)
|
||||
QuestieAnnounce:AbandonedQuest(questId)
|
||||
questLog[questId] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---Fires when the quest log changed in any way. This event fires very often!
|
||||
function _QuestEventHandler:QuestLogUpdate()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_LOG_UPDATE")
|
||||
|
||||
local continueQueuing = true
|
||||
-- Some of the other quest event didn't have the required information and ordered to wait for the next QLU.
|
||||
-- We are now calling the function which the event added.
|
||||
while continueQueuing and next(questLogUpdateQueue) do
|
||||
continueQueuing = _QuestLogUpdateQueue:GetFirst()()
|
||||
end
|
||||
|
||||
if doFullQuestLogScan then
|
||||
doFullQuestLogScan = false
|
||||
-- Function call updates doFullQuestLogScan. Order matters.
|
||||
_QuestEventHandler:UpdateAllQuests()
|
||||
else
|
||||
_QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Fires whenever a quest objective progressed
|
||||
---@param questId number
|
||||
function _QuestEventHandler:QuestWatchUpdate(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_WATCH_UPDATE", questId)
|
||||
|
||||
-- We do a full scan even though we have the questId because many QUEST_WATCH_UPDATE can fire before
|
||||
-- a QUEST_LOG_UPDATE. Also not every QUEST_WATCH_UPDATE gets a single QUEST_LOG_UPDATE and doing a full
|
||||
-- scan is less error prone
|
||||
doFullQuestLogScan = true
|
||||
end
|
||||
|
||||
local _UnitQuestLogChangedCallback = function()
|
||||
-- We also check in here because UNIT_QUEST_LOG_CHANGED is fired before the relevant events
|
||||
-- (Accept, removed, ...)
|
||||
if (not skipNextUQLCEvent) then
|
||||
doFullQuestLogScan = true
|
||||
else
|
||||
doFullQuestLogScan = false
|
||||
skipNextUQLCEvent = false
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Skipping UnitQuestLogChanged")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Fires when an objective changed in the quest log of the unitTarget. The required data is not available yet though
|
||||
---@param unitTarget string
|
||||
function _QuestEventHandler:UnitQuestLogChanged(unitTarget)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] UNIT_QUEST_LOG_CHANGED", unitTarget)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] UNIT_QUEST_LOG_CHANGED - skipNextUQLCEvent - ", skipNextUQLCEvent)
|
||||
|
||||
-- There seem to be quests which don't trigger a QUEST_WATCH_UPDATE.
|
||||
-- We don't add a full check to the queue if skipNextUQLCEvent == true (from QUEST_WATCH_UPDATE or QUEST_TURNED_IN)
|
||||
if (not skipNextUQLCEvent) then
|
||||
doFullQuestLogScan = true
|
||||
_QuestLogUpdateQueue:Insert(_UnitQuestLogChangedCallback)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Skipping UnitQuestLogChanged")
|
||||
end
|
||||
skipNextUQLCEvent = false
|
||||
end
|
||||
|
||||
|
||||
-- Fallback cleanup: some servers remove quests without firing QUEST_REMOVED reliably.
|
||||
-- This compares Questie's currentQuestlog vs the game's quest log and removes stale quests.
|
||||
function _QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
local gameQuestIds = {}
|
||||
local numEntries = select(1, GetNumQuestLogEntries()) or 0
|
||||
for questLogIndex = 1, numEntries do
|
||||
local title, _, _, isHeader, _, _, _, qid = GetQuestLogTitle(questLogIndex)
|
||||
if title and qid and qid > 0 and (not isHeader) then
|
||||
gameQuestIds[qid] = true
|
||||
end
|
||||
end
|
||||
|
||||
if QuestiePlayer and QuestiePlayer.currentQuestlog then
|
||||
local removedQuestIds = {}
|
||||
for questId in pairs(QuestiePlayer.currentQuestlog) do
|
||||
if questId and questId > 0 and (not gameQuestIds[questId]) then
|
||||
removedQuestIds[#removedQuestIds + 1] = questId
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #removedQuestIds do
|
||||
local questId = removedQuestIds[i]
|
||||
|
||||
-- Quest disappeared from log (abandoned or auto-turned-in)
|
||||
-- Check if this quest was confirmed as turned in (not just objectives complete)
|
||||
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
|
||||
local wasAlreadyComplete = Questie.db.char.complete and Questie.db.char.complete[questId]
|
||||
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
-- Only mark as complete if it was actually turned in OR already marked complete from previous session
|
||||
-- Don't use quest.WasComplete because that's set when objectives complete, not when quest is turned in
|
||||
if wasTurnedIn or wasAlreadyComplete then
|
||||
QuestieQuest:CompleteQuest(questId)
|
||||
else
|
||||
QuestieQuest:AbandonedQuest(questId)
|
||||
QuestieJourney:AbandonQuest(questId)
|
||||
QuestieAnnounce:AbandonedQuest(questId)
|
||||
end
|
||||
|
||||
questLog[questId] = nil
|
||||
end
|
||||
|
||||
if #removedQuestIds > 0 then
|
||||
QuestieNameplate:UpdateNameplate()
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Does a full scan of the quest log and updates every quest that is in the QUEST_ACCEPTED state and which hash changed
|
||||
--- since the last check
|
||||
function _QuestEventHandler:UpdateAllQuests()
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Running full questlog check")
|
||||
local questIdsToCheck = {}
|
||||
|
||||
-- TODO replace with a ready table so no need to generate at each call
|
||||
for questId, data in pairs(questLog) do
|
||||
if data.state == QUEST_LOG_STATES.QUEST_ACCEPTED then
|
||||
questIdsToCheck[questId] = true
|
||||
end
|
||||
end
|
||||
|
||||
local cacheMiss, changes = QuestLogCache.CheckForChanges(questIdsToCheck)
|
||||
|
||||
if next(changes) then
|
||||
for questId, objIds in pairs(changes) do
|
||||
--Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "objectives:", table.concat(objIds, ","), "will be updated")
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "will be updated")
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
QuestieNameplate:UpdateNameplate()
|
||||
QuestieQuest:UpdateQuest(questId)
|
||||
end
|
||||
QuestieCombatQueue:Queue(function()
|
||||
C_Timer.After(1.0, function()
|
||||
QuestieTracker:Update()
|
||||
end)
|
||||
end)
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_INFO, "Nothing to update")
|
||||
end
|
||||
|
||||
|
||||
_QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
|
||||
-- Do UpdateAllQuests() again at next QUEST_LOG_UPDATE if there was "cacheMiss" (game's cache and addon's cache didn't have all required data yet)
|
||||
doFullQuestLogScan = doFullQuestLogScan or cacheMiss
|
||||
end
|
||||
|
||||
local lastTimeQuestRelatedFrameClosedEvent = -1
|
||||
--- Blizzard does not fire any event when quest items are received or retrieved from sources other than looting.
|
||||
--- So we hook events which fires once or twice after closing certain frames and do a full quest log check.
|
||||
function _QuestEventHandler:QuestRelatedFrameClosed(event)
|
||||
local now = math.floor(GetTime())
|
||||
-- Don't do update if event fired twice
|
||||
if lastTimeQuestRelatedFrameClosedEvent ~= now then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event]", event)
|
||||
|
||||
lastTimeQuestRelatedFrameClosedEvent = now
|
||||
_QuestEventHandler:UpdateAllQuests()
|
||||
QuestieTracker:Update()
|
||||
end
|
||||
end
|
||||
|
||||
function _QuestEventHandler:ReputationChange()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] CHAT_MSG_COMBAT_FACTION_CHANGE")
|
||||
|
||||
-- Reputational quest progression doesn't fire UNIT_QUEST_LOG_CHANGED event, only QUEST_LOG_UPDATE event.
|
||||
doFullQuestLogScan = true
|
||||
end
|
||||
|
||||
--- Helper function to insert a callback to the questLogUpdateQueue and increase the index
|
||||
function _QuestLogUpdateQueue:Insert(callback)
|
||||
questLogUpdateQueue[questLogUpdateQueueSize] = callback
|
||||
questLogUpdateQueueSize = questLogUpdateQueueSize + 1
|
||||
end
|
||||
|
||||
--- Helper function to retrieve the first element of questLogUpdateQueue
|
||||
---@return function @The callback that was inserted first into questLogUpdateQueue
|
||||
function _QuestLogUpdateQueue:GetFirst()
|
||||
questLogUpdateQueueSize = questLogUpdateQueueSize - 1
|
||||
return tableRemove(questLogUpdateQueue, 1)
|
||||
end
|
||||
|
||||
local trackerMinimizedByDungeon = false
|
||||
function _QuestEventHandler:ZoneChangedNewArea()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[EVENT] ZONE_CHANGED_NEW_AREA")
|
||||
-- By my tests it takes a full 6-7 seconds for the world to load. There are a lot of
|
||||
-- backend Questie updates that occur when a player zones in/out of an instance. This
|
||||
-- is necessary to get everything back into it's "normal" state after all the updates.
|
||||
local isInInstance, instanceType = IsInInstance()
|
||||
|
||||
if isInInstance then
|
||||
C_Timer.After(8, function()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[EVENT] ZONE_CHANGED_NEW_AREA: Entering Instance")
|
||||
if Questie.db.profile.hideTrackerInDungeons then
|
||||
trackerMinimizedByDungeon = true
|
||||
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieTracker:Collapse()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
-- We only want this to fire outside of an instance if the player isn't dead and we need to reset the Tracker
|
||||
elseif (not Questie.db.char.isTrackerExpanded and not UnitIsGhost("player")) and trackerMinimizedByDungeon == true then
|
||||
C_Timer.After(8, function()
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[EVENT] ZONE_CHANGED_NEW_AREA: Exiting Instance")
|
||||
if Questie.db.profile.hideTrackerInDungeons then
|
||||
trackerMinimizedByDungeon = false
|
||||
|
||||
QuestieCombatQueue:Queue(function()
|
||||
QuestieTracker:Expand()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Is executed whenever an event is fired and triggers relevant event handling.
|
||||
---@param event string
|
||||
function _QuestEventHandler:OnEvent(event, ...)
|
||||
if event == "QUEST_ACCEPTED" then
|
||||
_QuestEventHandler:QuestAccepted(...)
|
||||
elseif event == "QUEST_TURNED_IN" then
|
||||
_QuestEventHandler:QuestTurnedIn(...)
|
||||
elseif event == "QUEST_REMOVED" then
|
||||
_QuestEventHandler:QuestRemoved(...)
|
||||
elseif event == "QUEST_LOG_UPDATE" then
|
||||
_QuestEventHandler:QuestLogUpdate()
|
||||
elseif event == "QUEST_WATCH_UPDATE" then
|
||||
_QuestEventHandler:QuestWatchUpdate(...)
|
||||
elseif event == "UNIT_QUEST_LOG_CHANGED" and select(1, ...) == "player" then
|
||||
_QuestEventHandler:UnitQuestLogChanged(...)
|
||||
elseif event == "ZONE_CHANGED_NEW_AREA" then
|
||||
_QuestEventHandler:ZoneChangedNewArea()
|
||||
elseif event == "NEW_RECIPE_LEARNED" then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[EVENT] NEW_RECIPE_LEARNED (QuestEventHandler)")
|
||||
doFullQuestLogScan = true -- If this event is related to a spell objective, a QUEST_LOG_UPDATE will be fired afterwards
|
||||
elseif event == "PLAYER_INTERACTION_MANAGER_FRAME_HIDE" then
|
||||
local eventType = select(1, ...)
|
||||
if eventType == 1 then
|
||||
event = "TRADE_CLOSED"
|
||||
elseif eventType == 5 then
|
||||
event = "MERCHANT_CLOSED"
|
||||
elseif eventType == 8 then
|
||||
event = "BANKFRAME_CLOSED"
|
||||
elseif eventType == 10 then
|
||||
event = "GUILDBANKFRAME_CLOSED"
|
||||
elseif eventType == 12 then
|
||||
event = "VENDOR_CLOSED"
|
||||
elseif eventType == 17 then
|
||||
event = "MAIL_CLOSED"
|
||||
elseif eventType == 21 then
|
||||
event = "AUCTION_HOUSE_CLOSED"
|
||||
else
|
||||
-- Unknown event which we will simply ignore
|
||||
return
|
||||
end
|
||||
_QuestEventHandler:QuestRelatedFrameClosed(event)
|
||||
elseif event == "CHAT_MSG_COMBAT_FACTION_CHANGE" then
|
||||
_QuestEventHandler:ReputationChange()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,349 @@
|
||||
--- Contains last known valid state of each quest in game's quest log, per quest.
|
||||
--- I.E. All data related to a quest is valid.
|
||||
--- Includes a "hack" to have correct objectives' progress while quest isComplete = 1. Otherwise it would need to be done everywhere else in code
|
||||
---@class QuestLogCache
|
||||
local QuestLogCache = QuestieLoader:CreateModule("QuestLogCache")
|
||||
---@type QuestieLib
|
||||
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
|
||||
---@type Sounds
|
||||
local Sounds = QuestieLoader:ImportModule("Sounds")
|
||||
|
||||
--- COMPATIBILITY ---
|
||||
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
||||
local C_QuestLog_GetQuestObjectives = QuestieCompat.C_QuestLog.GetQuestObjectives
|
||||
local HaveQuestData = QuestieCompat.HaveQuestData
|
||||
|
||||
local stringByte = string.byte
|
||||
|
||||
-- 3 * (Max possible number of quests in game quest log)
|
||||
-- This is a safe value, even smaller would be enough. Too large won't effect performance
|
||||
local MAX_QUEST_LOG_INDEX = 75
|
||||
|
||||
--[[
|
||||
Example of data in cache table.
|
||||
raw_* are as in game's quest log. Their non-raw versions are corrected/modified for addon's easy use.
|
||||
|
||||
local cache = {
|
||||
[questId] = {
|
||||
title = "Quest name",
|
||||
questTag = "Dungeon", -- nil, "Dungeon", "Raid", etc.
|
||||
isComplete = nil,
|
||||
objectives = {
|
||||
{
|
||||
text = "Objective Text"
|
||||
type = "monster",
|
||||
finished = false,
|
||||
numFulfilled = 2,
|
||||
numRequired = 3,
|
||||
raw_Text = "Objective Text slain: 2/3",
|
||||
raw_finished = false
|
||||
raw_numFulfilled = 2,
|
||||
},
|
||||
{
|
||||
text = "Objective2"
|
||||
type = "item",
|
||||
finished = false,
|
||||
numFulfilled = 0,
|
||||
numRequired = 5,
|
||||
raw_text = "Objective2 : 0/5",
|
||||
raw_finished = false,
|
||||
raw_numFulfilled = 0,
|
||||
},
|
||||
....
|
||||
},
|
||||
[questId2] = ....,
|
||||
}
|
||||
]]--
|
||||
|
||||
|
||||
---@class QuestLogCacheObjectiveData
|
||||
---@field text string "Objective Text"
|
||||
---@field type "monster"|"object"|"item"|"reputation"|"killcredit"|"event"|"spell"
|
||||
---@field finished boolean
|
||||
---@field numFulfilled number
|
||||
---@field numRequired number
|
||||
---@field raw_Text string E.g "Objective Text slain: 2/3",
|
||||
---@field raw_finished boolean
|
||||
---@field raw_numFulfilled number
|
||||
|
||||
---@class QuestLogCacheData
|
||||
---@field title string
|
||||
---@field questTag QuestTag
|
||||
---@field isComplete -1|0|1 @ -1 = failed, 0 = not complete, 1 = complete
|
||||
---@field objectives QuestLogCacheObjectiveData[]
|
||||
|
||||
|
||||
---@type table<QuestId, QuestLogCacheData>
|
||||
local cache = {}
|
||||
|
||||
--- NEVER EVER EDIT this table outside of the QuestLogCache module! !!!
|
||||
---@type table<QuestId, QuestLogCacheData>
|
||||
QuestLogCache.questLog_DO_NOT_MODIFY = cache
|
||||
|
||||
|
||||
|
||||
---@return table? newObjectives, ObjectiveIndex[] changedObjIds @nil == cache miss in both addon and game caches. table {} == no objectives.
|
||||
local function GetNewObjectives(questId, oldObjectives, questLogIndex)
|
||||
local newObjectives = {} -- creating a fresh one to be able revert to old easily in case of missing data
|
||||
local changedObjIds -- not assigning {} for easier nil when nothing changed
|
||||
local objectives = C_QuestLog_GetQuestObjectives(questId, questLogIndex)
|
||||
|
||||
for objIndex=1, #objectives do -- iterate manually to be sure getting those in order
|
||||
local oldObj = oldObjectives[objIndex]
|
||||
local newObj = objectives[objIndex]
|
||||
-- Check if objective.text is in game's cache
|
||||
if (newObj.text) and (stringByte(newObj.text, 1) ~= 32) then
|
||||
-- Check if objective has changed
|
||||
if oldObj and oldObj.raw_numFulfilled == newObj.numFulfilled and oldObj.raw_text == newObj.text and oldObj.raw_finished == newObj.finished and oldObj.numRequired == newObj.numRequired and oldObj.type == newObj.type then
|
||||
-- Not changed
|
||||
newObjectives[objIndex] = oldObj
|
||||
else
|
||||
-- objective has changed, add it to list of change ones
|
||||
if (not changedObjIds) then
|
||||
changedObjIds = { objIndex }
|
||||
else
|
||||
changedObjIds[#changedObjIds+1] = objIndex
|
||||
end
|
||||
|
||||
if oldObj and newObj and oldObj.numRequired ~= oldObj.numFulfilled and newObj.numRequired == newObj.numFulfilled then
|
||||
Sounds.PlayObjectiveComplete()
|
||||
end
|
||||
|
||||
if oldObj and newObj and oldObj.numRequired ~= oldObj.numFulfilled and newObj.numRequired ~= newObj.numFulfilled then
|
||||
Sounds.PlayObjectiveProgress()
|
||||
end
|
||||
|
||||
newObjectives[objIndex] = {
|
||||
raw_text = newObj.text,
|
||||
raw_finished = newObj.finished,
|
||||
raw_numFulfilled = newObj.numFulfilled,
|
||||
type = newObj.type,
|
||||
numRequired = newObj.numRequired,
|
||||
text = QuestieLib.TrimObjectiveText(newObj.text, newObj.type),
|
||||
finished = newObj.finished, -- gets overwritten with correct value later if quest isComplete
|
||||
numFulfilled = newObj.numFulfilled, -- gets overwritten with correct value later if quest isComplete
|
||||
}
|
||||
end
|
||||
else -- objective text not in game's cache
|
||||
if oldObj then
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[GetNewObjectives] objective not in game's cache. Using addon's cache. questID, objIndex:", questId, objIndex)
|
||||
-- Extremely unlikely that the objective has changed from cached version as a change SHOULD trigger fetching data into game cache.
|
||||
-- Possible bug point if there comes desync issues.
|
||||
newObjectives[objIndex] = oldObj
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[GetNewObjectives] \"WARNING\" objective not in game's cache nor addon's cache. questID, objIndex:", questId, objIndex)
|
||||
-- Objective has been never cached
|
||||
-- Tell to function caller that we couldn't get all required data from game's cache
|
||||
-- Don't loop rest of objectives as we won't anyway save those into cache[] and C_QuestLog.GetQuestObjectives() call already triggered game to initiate caching those into game's cache.
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return newObjectives, changedObjIds
|
||||
end
|
||||
|
||||
-- For profiling
|
||||
QuestLogCache._GetNewObjectives = GetNewObjectives
|
||||
|
||||
--- Updates questlogcache.
|
||||
--- Remember to handle returned changes table even when cacheMiss == true. Returned changes are still valid. There may just be more changes that we couldn't get yet.
|
||||
--- Called only from QuestEventHandler.
|
||||
---@param questIdsToCheck table? @keys are the questIds
|
||||
---@return boolean cacheMiss, table changes @cacheMiss = couldn't get all required data ; changes[questId] = list of changed objectiveIndexes (may be an empty list if quest has no objectives)
|
||||
function QuestLogCache.CheckForChanges(questIdsToCheck)
|
||||
local cacheMiss = false
|
||||
local changes = {}
|
||||
local questIdsChecked = {}
|
||||
|
||||
local numEntries = select(1, GetNumQuestLogEntries()) or 0
|
||||
for questLogIndex = 1, numEntries do
|
||||
local title, _, questTag, isHeader, _, isComplete, _, questId = GetQuestLogTitle(questLogIndex)
|
||||
|
||||
-- Skip weird/header entries / questId=0 (these happen a lot on your server)
|
||||
if title and questId and questId > 0 and (not isHeader) then
|
||||
if (not questIdsToCheck) or questIdsToCheck[questId] then
|
||||
questIdsChecked[questId] = true
|
||||
|
||||
if HaveQuestData(questId) then
|
||||
local cachedQuest = cache[questId]
|
||||
local cachedObjectives = cachedQuest and cachedQuest.objectives or {}
|
||||
|
||||
local newObjectives, changedObjIds = GetNewObjectives(questId, cachedObjectives, questLogIndex)
|
||||
|
||||
if newObjectives then
|
||||
if (not cachedQuest) or (#cachedObjectives == #newObjectives and #cachedObjectives > 0 and
|
||||
(cachedQuest.title ~= title or cachedQuest.questTag ~= questTag or cachedQuest.isComplete ~= isComplete)) then
|
||||
|
||||
changedObjIds = {}
|
||||
for i = 1, #newObjectives do
|
||||
changedObjIds[i] = i
|
||||
end
|
||||
|
||||
if isComplete == 1 then
|
||||
for i = 1, #newObjectives do
|
||||
local o = newObjectives[i]
|
||||
o.finished = true
|
||||
o.numFulfilled = o.numRequired
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cachedQuest and (not cachedQuest.isComplete) and isComplete == 1 then
|
||||
Sounds.PlayQuestComplete()
|
||||
end
|
||||
|
||||
if changedObjIds then
|
||||
cache[questId] = {
|
||||
title = title,
|
||||
questTag = questTag,
|
||||
isComplete = isComplete,
|
||||
objectives = newObjectives,
|
||||
}
|
||||
changes[questId] = changedObjIds
|
||||
end
|
||||
else
|
||||
cacheMiss = true
|
||||
end
|
||||
else
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestLogCache.CheckForChanges] HaveQuestData() == false. questId, index:", questId, questLogIndex)
|
||||
C_QuestLog_GetQuestObjectives(questId, questLogIndex)
|
||||
cacheMiss = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Debug / warning: ignore questId=0 and don't treat it as "missing" when the log has weird entries
|
||||
if questIdsToCheck then
|
||||
for questId in pairs(questIdsToCheck) do
|
||||
if questId and questId > 0 and (not questIdsChecked[questId]) then
|
||||
Questie:Warning("Please report on Github or Discord. QuestId doesn't exist in Game's quest log:", questId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return cacheMiss, changes
|
||||
end
|
||||
|
||||
|
||||
|
||||
function QuestLogCache.RemoveQuest(questId)
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestLogCache.RemoveQuest] remove questId:", questId)
|
||||
cache[questId] = nil
|
||||
end
|
||||
|
||||
|
||||
--- Tests if game client's cache has all quest log quests and objectives cached.
|
||||
--- Avoid using this function if possible.
|
||||
---@return boolean gameCacheOK
|
||||
function QuestLogCache.TestGameCache()
|
||||
local gameCacheOK = true
|
||||
for questLogIndex = 1, MAX_QUEST_LOG_INDEX do
|
||||
local title, _, _, isHeader, _, _, _, questId = GetQuestLogTitle(questLogIndex)
|
||||
if (not title) then
|
||||
break -- We exceeded the valid quest log entries
|
||||
end
|
||||
if (not isHeader) then
|
||||
if HaveQuestData(questId) then
|
||||
local objectives = C_QuestLog_GetQuestObjectives(questId, questLogIndex)
|
||||
|
||||
for objIndex=1, #objectives do
|
||||
local text = objectives[objIndex].text
|
||||
-- Check if objective.text is not in game's cache
|
||||
if (not text) or (stringByte(text, 1) == 32) then
|
||||
gameCacheOK = false
|
||||
end
|
||||
end
|
||||
else
|
||||
gameCacheOK = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestLogCache.TestGameCache]", (gameCacheOK and "Cache ok." or "Cache missing data."))
|
||||
return gameCacheOK
|
||||
end
|
||||
|
||||
|
||||
--- A wrapper function to add error check instead using exposed table directly.
|
||||
---@param questId QuestId
|
||||
---@return QuestLogCacheData? @NEVER EVER MODIFY THE RETURNED TABLE
|
||||
function QuestLogCache.GetQuest(questId)
|
||||
-- Fix the issue at function caller side if this error pops up.
|
||||
if (not cache[questId]) then
|
||||
Questie:Print(debugstack(1, 20, 4))
|
||||
Questie:Error("Please report this error. GetQuest: The quest doesn't exist in QuestLogCache.", questId)
|
||||
return
|
||||
end
|
||||
return cache[questId]
|
||||
end
|
||||
|
||||
--- A wrapper function to add error check instead using exposed table directly.
|
||||
---@param questId QuestId
|
||||
---@return table<ObjectiveIndex, QuestLogCacheObjectiveData>? @NEVER EVER MODIFY THE RETURNED TABLE
|
||||
function QuestLogCache.GetQuestObjectives(questId)
|
||||
-- Fix the issue at function caller side if this error pops up.
|
||||
if (not cache[questId]) then
|
||||
Questie:Print(debugstack(1, 20, 4))
|
||||
Questie:Error("Please report this error. GetQuestObjectives: The quest doesn't exist in QuestLogCache.", questId)
|
||||
return
|
||||
end
|
||||
return cache[questId].objectives
|
||||
end
|
||||
|
||||
|
||||
|
||||
---@param q table @quest
|
||||
---@param i number @index of the objective
|
||||
---@param o table @objective
|
||||
local function DebugPrintObjective(q, i, o)
|
||||
if (o.raw_numFulfilled == o.numFulfilled) and (o.raw_finished == o.finished) then
|
||||
print(" ", i.."/"..#q.objectives..":",
|
||||
o.numFulfilled.."/"..o.numRequired.."="..tostring(o.finished),
|
||||
o.type,
|
||||
"\""..o.raw_text.."\" \""..o.text.."\"")
|
||||
else
|
||||
print(" ", i.."/"..#q.objectives..":",
|
||||
o.raw_numFulfilled.."/"..o.numRequired.."="..tostring(o.raw_finished),
|
||||
"FIX:", o.numFulfilled.."/"..o.numRequired.."="..tostring(o.finished),
|
||||
o.type,
|
||||
"\""..o.raw_text.."\" \""..o.text.."\"")
|
||||
end
|
||||
end
|
||||
|
||||
--- Debug function, prints whole cache
|
||||
function QuestLogCache.DebugPrintCache()
|
||||
print("DebugPrintCache", GetTime())
|
||||
local count = 0
|
||||
for questId, q in pairs(cache) do
|
||||
count = count + 1
|
||||
print("Quest: ("..questId..") \""..q.title.."\" questTag="..tostring(q.questTag) ,"isComplete="..tostring(q.isComplete))
|
||||
if not next(q.objectives) then
|
||||
print(" no objectives")
|
||||
else
|
||||
for i, o in ipairs(q.objectives) do
|
||||
DebugPrintObjective(q, i, o)
|
||||
end
|
||||
end
|
||||
end
|
||||
print("Total Quests ", count)
|
||||
end
|
||||
|
||||
--- Debug function, prints changes
|
||||
function QuestLogCache.DebugPrintCacheChanges(cacheMiss, changes)
|
||||
local highlight = ((not cacheMiss) and (not next(changes))) or (cacheMiss and next(changes)) -- highlight untypical cases. they are okey, but sometimes interesting.
|
||||
print("DebugPrintCacheChanges", GetTime(), (highlight and "\124cffFF4444CacheMiss:\124r" or "CacheMiss"), cacheMiss)
|
||||
|
||||
for questId, objIndexes in pairs(changes) do
|
||||
local q = cache[questId]
|
||||
print("Quest: ("..questId..") \""..q.title.."\" questTag="..tostring(q.questTag) ,"isComplete="..tostring(q.isComplete))
|
||||
if not next(objIndexes) then
|
||||
print(" no objectives changed (or quest doesn't have objectives)")
|
||||
else
|
||||
for _, i in ipairs(objIndexes) do
|
||||
DebugPrintObjective(q, i, q.objectives[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,191 @@
|
||||
---@class QuestgiverFrame
|
||||
local QuestgiverFrame = QuestieLoader:CreateModule("QuestgiverFrame")
|
||||
|
||||
---@type QuestieDB
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
|
||||
---@type QuestieLib
|
||||
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
|
||||
|
||||
--- COMPATIBILITY ---
|
||||
local UnitGUID = QuestieCompat.UnitGUID
|
||||
|
||||
local _G = _G
|
||||
local tinsert = tinsert
|
||||
local MAX_NUM_QUESTS = MAX_NUM_QUESTS
|
||||
|
||||
-- This is the logic used for determining which icon we should show for a quest
|
||||
-- This just determines the "type" of icon shown, not the exact icon file - see Questie.icons
|
||||
---@param questID number
|
||||
---@param isActive boolean
|
||||
---@return string
|
||||
local function determineAppropriateQuestIcon(questID, isActive)
|
||||
if questID == 0 then -- if we were fed a questID of 0, the ID bruteforce failed, abort
|
||||
if isActive == true then
|
||||
return Questie.icons["complete"]
|
||||
else
|
||||
return Questie.icons["available"]
|
||||
end
|
||||
end
|
||||
local icon = Questie.icons["available"] -- fallback icon in case any of the logic below fails
|
||||
if isActive == true then
|
||||
icon = Questie.icons["incomplete"] -- fallback icon in case any of the logic below fails
|
||||
if QuestieDB.IsComplete(questID) == 1 then
|
||||
if QuestieDB.IsPvPQuest(questID) then
|
||||
icon = Questie.icons["pvpquest_complete"]
|
||||
elseif QuestieDB.IsActiveEventQuest(questID) then
|
||||
icon = Questie.icons["eventquest_complete"]
|
||||
elseif QuestieDB.IsRepeatable(questID) then
|
||||
icon = Questie.icons["repeatable_complete"]
|
||||
else
|
||||
icon = Questie.icons["complete"]
|
||||
end
|
||||
end
|
||||
else
|
||||
if QuestieDB.IsPvPQuest(questID) then
|
||||
icon = Questie.icons["pvpquest"]
|
||||
elseif QuestieDB.IsActiveEventQuest(questID) then
|
||||
icon = Questie.icons["eventquest"]
|
||||
elseif QuestieDB.IsRepeatable(questID) then
|
||||
icon = Questie.icons["repeatable"]
|
||||
end
|
||||
end
|
||||
return icon
|
||||
end
|
||||
|
||||
-- 9.0.0 API GOSSIP
|
||||
local function updateGossipFrame()
|
||||
local numAvailable = GetNumGossipAvailableQuests()
|
||||
local numActive = GetNumGossipActiveQuests()
|
||||
local availQuests = {QuestieCompat.GetAvailableQuests()}
|
||||
local activeQuests = {QuestieCompat.GetActiveQuests()}
|
||||
local index = 0 -- this variable tracks the GossipTitleButton we should be targeting for icon changes
|
||||
local questgiver = UnitGUID("npc")
|
||||
if numAvailable > 0 then
|
||||
for i=1, numAvailable do
|
||||
index = index + 1
|
||||
-- GetGossipAvailableQuests() returns 7 individual values per quest entry...
|
||||
-- so we have to filter out to every 7th value, starting with 1, 8, 15, etc
|
||||
local questIndex = (1 + ((i - 1) * 7))
|
||||
local questname = availQuests[questIndex]
|
||||
local questid = QuestieDB.GetQuestIDFromName(questname, questgiver, true)
|
||||
local gossipIcon = _G["GossipTitleButton" .. index .. "GossipIcon"]
|
||||
gossipIcon:SetTexture(determineAppropriateQuestIcon(questid, false))
|
||||
end
|
||||
-- each new section in a gossip frame has an offset of 1, so for instance, with 2 quests shown,
|
||||
-- 1 active 1 available, the available will be GossipTitleButton1 and the active will be GossipTitleButton3
|
||||
if numActive > 0 then index = index + 1 end
|
||||
end
|
||||
if numActive > 0 then
|
||||
for i=1, numActive do
|
||||
index = index + 1
|
||||
-- GetGossipActiveQuests() returns 6 individual values per quest entry...
|
||||
-- so we have to filter out to every 6th value, starting with 1, 7, 13, etc
|
||||
local questIndex = (1 + ((i - 1) * 6))
|
||||
local questname = activeQuests[questIndex]
|
||||
local questid = QuestieDB.GetQuestIDFromName(questname, questgiver, false)
|
||||
local gossipIcon = _G["GossipTitleButton" .. index .. "GossipIcon"]
|
||||
gossipIcon:SetTexture(determineAppropriateQuestIcon(questid, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- GREETING FRAMES (API independent)
|
||||
local function updateGreetingFrame()
|
||||
local titleLines = {}
|
||||
local questIconTextures = {}
|
||||
local questgiver = UnitGUID("npc")
|
||||
for i = 1, MAX_NUM_QUESTS do
|
||||
local titleLine = _G["QuestTitleButton" .. i]
|
||||
if titleLine then
|
||||
tinsert(titleLines, titleLine)
|
||||
tinsert(questIconTextures, _G[titleLine:GetName() .. "QuestIcon"])
|
||||
else
|
||||
Questie:Error("Frame error! Could not obtain Greeting's QuestTitleButton object. Please report this on Github or Discord!")
|
||||
Questie:Error("Questgiver is: " .. questgiver)
|
||||
Questie:Error("Client info is: " .. GetBuildInfo() .. "; " .. QuestieLib:GetAddonVersionString())
|
||||
return
|
||||
end
|
||||
end
|
||||
for i, titleLine in ipairs(titleLines) do
|
||||
if (titleLine:IsVisible()) then
|
||||
local lineIcon = questIconTextures[i]
|
||||
-- determining if the current line is a "Current" quest or "Available" quest is important
|
||||
-- because we have to use different API calls to obtain their quest titles
|
||||
if (titleLine.isActive == 1) then
|
||||
lineIcon:SetTexture(Questie.icons["incomplete"]) -- fallback icon in case any of the logic below fails
|
||||
local title = GetActiveTitle(titleLine:GetID()) -- obtain plaintext name of quest
|
||||
local questID = QuestieDB.GetQuestIDFromName(title, questgiver, false)
|
||||
local icon = determineAppropriateQuestIcon(questID, true)
|
||||
lineIcon:SetTexture(icon)
|
||||
else
|
||||
lineIcon:SetTexture(Questie.icons["available"]) -- fallback icon in case any of the logic below fails
|
||||
local title = GetAvailableTitle(titleLine:GetID())
|
||||
local questID = QuestieDB.GetQuestIDFromName(title, questgiver, true)
|
||||
local icon = determineAppropriateQuestIcon(questID, false)
|
||||
lineIcon:SetTexture(icon)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function QuestgiverFrame.GossipMark()
|
||||
if Questie.db.profile.enableQuestFrameIcons == true then
|
||||
if GossipAvailableQuestButtonMixin then -- This call is added with Dragonflight (10.0.0) API, use if available
|
||||
return -- This call is automatically hooked, no need to run a function
|
||||
else -- If DF API not available, use Shadowlands (9.0.0) method
|
||||
updateGossipFrame()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function QuestgiverFrame.GreetingMark()
|
||||
if Questie.db.profile.enableQuestFrameIcons == true then
|
||||
updateGreetingFrame()
|
||||
end
|
||||
end
|
||||
|
||||
-- 10.0.0 API GOSSIP
|
||||
-- Boy, this code is clean... these DF Gossip APIs sure are great!
|
||||
-- What a shame that the greeting API hasn't been touched in two decades.
|
||||
if GossipAvailableQuestButtonMixin then
|
||||
local oldAvailableSetup = GossipAvailableQuestButtonMixin.Setup
|
||||
function GossipAvailableQuestButtonMixin:Setup(...)
|
||||
oldAvailableSetup(self, ...)
|
||||
if (not Questie.started) then
|
||||
return
|
||||
end
|
||||
|
||||
if self.GetElementData ~= nil and Questie.db.profile.enableQuestFrameIcons == true then
|
||||
local id = self.GetElementData().info.questID
|
||||
if id then
|
||||
self.Icon:SetTexture(determineAppropriateQuestIcon(id, false))
|
||||
else
|
||||
Questie:Error("Frame error! Missing Gossip line item quest ID. Please report this on Github or Discord!")
|
||||
Questie:Error("Questgiver for available quest is: " .. UnitGUID("npc"))
|
||||
Questie:Error("Client info is: " .. GetBuildInfo() .. "; " .. QuestieLib:GetAddonVersionString())
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local oldActiveSetup = GossipActiveQuestButtonMixin.Setup
|
||||
function GossipActiveQuestButtonMixin:Setup(...)
|
||||
oldActiveSetup(self, ...)
|
||||
if (not Questie.started) then
|
||||
return
|
||||
end
|
||||
|
||||
if self.GetElementData ~= nil and Questie.db.profile.enableQuestFrameIcons == true then
|
||||
local id = self.GetElementData().info.questID
|
||||
if id then
|
||||
self.Icon:SetTexture(determineAppropriateQuestIcon(id, true))
|
||||
else
|
||||
Questie:Error("Frame error! Missing Gossip line item quest ID. Please report this on Github or Discord!")
|
||||
Questie:Error("Questgiver for active quest is: " .. UnitGUID("npc"))
|
||||
Questie:Error("Client info is: " .. GetBuildInfo() .. "; " .. QuestieLib:GetAddonVersionString())
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,297 @@
|
||||
---@type QuestieQuest
|
||||
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
|
||||
---@type QuestieQuestPrivate
|
||||
QuestieQuest.private = QuestieQuest.private or {}
|
||||
---@class QuestieQuestPrivate
|
||||
local _QuestieQuest = QuestieQuest.private
|
||||
|
||||
---@type QuestieDB
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
---@type QuestieCorrections
|
||||
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
|
||||
|
||||
local function _GetIconScaleForMonster()
|
||||
return Questie.db.profile.monsterScale or 1
|
||||
end
|
||||
|
||||
local function _GetIconScaleForObject()
|
||||
return Questie.db.profile.objectScale or 1
|
||||
end
|
||||
|
||||
local function _GetIconScaleForEvent()
|
||||
return Questie.db.profile.eventScale or 1.35
|
||||
end
|
||||
|
||||
local function _GetIconScaleForLoot()
|
||||
return Questie.db.profile.lootScale or 1
|
||||
end
|
||||
|
||||
|
||||
---@class SpawnListBase
|
||||
---@field Name string
|
||||
---@field Spawns table<AreaId, CoordPair[]>>
|
||||
---@field Icon string @Icon path
|
||||
---@field GetIconScale function Function to get the iconScale
|
||||
---@field IconScale number Initial value returned by the GetIconScale function
|
||||
|
||||
|
||||
---@class SpawnListTooltip
|
||||
---@field TooltipKey string
|
||||
|
||||
---@class SpawnListObject : SpawnListBase, SpawnListTooltip
|
||||
---@field Id ObjectId The ID of the Object
|
||||
|
||||
---@class SpawnListNPC : SpawnListBase, SpawnListTooltip
|
||||
---@field Id NpcId The ID of the NPC
|
||||
---@field Waypoints table<AreaId, CoordPair[]>
|
||||
---@field Hostile true|boolean
|
||||
|
||||
---@class SpawnListItem : SpawnListBase, SpawnListTooltip, SpawnListNPC, SpawnListObject
|
||||
---@field Id ObjectId|NpcId The ID of the Object or Npc
|
||||
---@field ItemId ItemId The ID of the item that the spawn drops
|
||||
|
||||
---@class SpawnListEvent : SpawnListBase
|
||||
---@field Id number The ID of the Event (Is this even used?)
|
||||
|
||||
local killcredit, monster, object, event, item, spell
|
||||
|
||||
---@type table<"killcredit"|"monster"|"object"|"event"|"item", function>
|
||||
_QuestieQuest.objectiveSpawnListCallTable = {}
|
||||
---comment
|
||||
---@param npcId NpcId
|
||||
---@param objective any
|
||||
---@param objectiveData KillObjective
|
||||
---@return table<NpcId, SpawnListNPC>[]
|
||||
killcredit = function(npcId, objective, objectiveData)
|
||||
---@type SpawnListNPC[]
|
||||
local ret = {}
|
||||
for npcIdIndex = 1, #objectiveData.IdList do
|
||||
local killCreditNpcId = objectiveData.IdList[npcIdIndex]
|
||||
ret[killCreditNpcId] = monster(killCreditNpcId, objective)[killCreditNpcId]
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param npcId any
|
||||
---@param objective any
|
||||
---@return table<NpcId, SpawnListNPC>?
|
||||
monster = function(npcId, objective)
|
||||
if (not npcId) then
|
||||
Questie:Error(
|
||||
"Corrupted objective data handed to objectiveSpawnListCallTable['monster']:",
|
||||
"'" .. objective.Description .. "' -",
|
||||
"Please report this error on Discord or GitHub."
|
||||
)
|
||||
return nil
|
||||
end
|
||||
|
||||
local name = QuestieDB.QueryNPCSingle(npcId, "name")
|
||||
if (not name) then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "Name missing for NPC:", npcId)
|
||||
return nil
|
||||
end
|
||||
|
||||
local spawns = QuestieDB.QueryNPCSingle(npcId, "spawns")
|
||||
if (not spawns) then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "Spawn data missing for NPC:", npcId)
|
||||
spawns = {}
|
||||
end
|
||||
|
||||
local rank = QuestieDB.QueryNPCSingle(npcId, "rank")
|
||||
|
||||
local enableSpawns = not QuestieCorrections.questNPCBlacklist[npcId]
|
||||
local enableWaypoints = enableSpawns and 2 ~= rank -- a rare mob spawn. todo: option for this
|
||||
|
||||
---@type SpawnListNPC
|
||||
local monster = {
|
||||
Id = npcId,
|
||||
Name = name,
|
||||
Spawns = enableSpawns and spawns or {},
|
||||
Waypoints = enableWaypoints and QuestieDB.QueryNPCSingle(npcId, "waypoints") or {},
|
||||
Hostile = true,
|
||||
Icon = Questie.ICON_TYPE_SLAY,
|
||||
GetIconScale = _GetIconScaleForMonster,
|
||||
IconScale = _GetIconScaleForMonster(),
|
||||
TooltipKey = "m_" .. npcId, -- todo: use ID based keys
|
||||
}
|
||||
|
||||
return {
|
||||
[npcId] = monster
|
||||
}
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param objectId any
|
||||
---@param objective any
|
||||
---@return table<ObjectId, SpawnListObject>?
|
||||
object = function(objectId, objective)
|
||||
if (not objectId) then
|
||||
Questie:Error(
|
||||
"Corrupted objective data handed to objectiveSpawnListCallTable['object']:",
|
||||
"'" .. objective.Description .. "' -",
|
||||
"Please report this error on Discord or GitHub."
|
||||
)
|
||||
return nil
|
||||
end
|
||||
|
||||
local name = QuestieDB.QueryObjectSingle(objectId, "name")
|
||||
if (not name) then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "Name missing for object:", objectId)
|
||||
return nil
|
||||
end
|
||||
|
||||
local spawns = QuestieDB.QueryObjectSingle(objectId, "spawns")
|
||||
if (not spawns) then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "Spawn data missing for object:", objectId)
|
||||
spawns = {}
|
||||
end
|
||||
|
||||
|
||||
---@type SpawnListObject
|
||||
local retObject = {
|
||||
Id = objectId,
|
||||
Name = name,
|
||||
Spawns = spawns,
|
||||
Icon = Questie.ICON_TYPE_OBJECT,
|
||||
GetIconScale = _GetIconScaleForObject,
|
||||
IconScale = _GetIconScaleForObject(),
|
||||
TooltipKey = "o_" .. objectId,
|
||||
}
|
||||
|
||||
return {
|
||||
[objectId] = retObject
|
||||
}
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param eventId any
|
||||
---@param objective any
|
||||
---@return { [1]: SpawnListEvent }?
|
||||
event = function(eventId, objective)
|
||||
local spawns = objective.Coordinates
|
||||
if (not spawns) then
|
||||
Questie:Error("Missing event data for Objective:", objective.Description, "id:", eventId)
|
||||
spawns = {}
|
||||
end
|
||||
|
||||
---@type SpawnListEvent
|
||||
local retEvent = {
|
||||
Id = eventId or 0,
|
||||
Name = objective.Description or "Event Trigger",
|
||||
Spawns = spawns,
|
||||
Icon = Questie.ICON_TYPE_EVENT,
|
||||
GetIconScale = _GetIconScaleForEvent,
|
||||
IconScale = _GetIconScaleForEvent(),
|
||||
}
|
||||
|
||||
return {
|
||||
[1] = retEvent
|
||||
}
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param itemId any
|
||||
---@param objective any
|
||||
---@return table<ItemId, SpawnListItem>?
|
||||
item = function(itemId, objective)
|
||||
if (not itemId) then
|
||||
Questie:Error(
|
||||
"Corrupted objective data handed to objectiveSpawnListCallTable['item']:",
|
||||
"'" .. objective.Description .. "' -",
|
||||
"Please report this error on Discord or GitHub."
|
||||
)
|
||||
return nil
|
||||
end
|
||||
|
||||
local ret = {}
|
||||
local item = QuestieDB:GetItem(itemId)
|
||||
if item and item.Sources and (not item.Hidden) then
|
||||
for _, source in pairs(item.Sources) do
|
||||
if _QuestieQuest.objectiveSpawnListCallTable[source.Type] and source.Type ~= "item" then -- anti-recursive-loop check, should never be possible but would be bad if it was
|
||||
local sourceList = _QuestieQuest.objectiveSpawnListCallTable[source.Type](source.Id, objective)
|
||||
if not sourceList then
|
||||
Questie:Error("Missing objective data for", source.Type, "'", objective, "'", source.Id)
|
||||
else
|
||||
for id, sourceData in pairs(sourceList) do
|
||||
if (not ret[id]) then
|
||||
local icon, GetIconScale
|
||||
if source.Type == "object" then
|
||||
icon = Questie.ICON_TYPE_OBJECT
|
||||
GetIconScale = _GetIconScaleForObject
|
||||
else
|
||||
icon = Questie.ICON_TYPE_LOOT
|
||||
GetIconScale = _GetIconScaleForLoot
|
||||
end
|
||||
|
||||
ret[id] = {
|
||||
Id = id,
|
||||
Name = sourceData.Name,
|
||||
Hostile = true,
|
||||
ItemId = item.Id,
|
||||
TooltipKey = sourceData.TooltipKey,
|
||||
Spawns = {},
|
||||
Waypoints = {},
|
||||
Icon = icon,
|
||||
GetIconScale = GetIconScale,
|
||||
IconScale = GetIconScale(),
|
||||
}
|
||||
end
|
||||
if sourceData.Spawns then
|
||||
local itemSpawns = ret[id].Spawns
|
||||
for zone, spawns in pairs(sourceData.Spawns) do
|
||||
if (not itemSpawns[zone]) then
|
||||
itemSpawns[zone] = {}
|
||||
end
|
||||
|
||||
local itemSpawnsInZone = itemSpawns[zone]
|
||||
for _, spawn in pairs(spawns) do
|
||||
itemSpawnsInZone[#itemSpawnsInZone+1] = spawn
|
||||
end
|
||||
end
|
||||
end
|
||||
if sourceData.Waypoints then
|
||||
local itemWaypoints = ret[id].Waypoints
|
||||
for zone, spawns in pairs(sourceData.Waypoints) do
|
||||
if (not itemWaypoints[zone]) then
|
||||
itemWaypoints[zone] = {}
|
||||
end
|
||||
|
||||
local itemWaypointsInZone = itemWaypoints[zone]
|
||||
for _, spawn in pairs(spawns) do
|
||||
itemWaypointsInZone[#itemWaypointsInZone+1] = spawn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param spellId number
|
||||
---@param objective any
|
||||
---@return table<ItemId, SpawnListItem>?
|
||||
spell = function(spellId, objective, objectiveData)
|
||||
if (not spellId) then
|
||||
Questie:Error(
|
||||
"Corrupted objective data handed to objectiveSpawnListCallTable['spell']:",
|
||||
"'" .. objective.Description .. "' -",
|
||||
"Please report this error on Discord or GitHub."
|
||||
)
|
||||
return nil
|
||||
end
|
||||
|
||||
local itemSource = objectiveData.ItemSourceId
|
||||
|
||||
return item(itemSource, objective)
|
||||
end
|
||||
|
||||
_QuestieQuest.objectiveSpawnListCallTable["killcredit"] = killcredit
|
||||
_QuestieQuest.objectiveSpawnListCallTable["monster"] = monster
|
||||
_QuestieQuest.objectiveSpawnListCallTable["object"] = object
|
||||
_QuestieQuest.objectiveSpawnListCallTable["event"] = event
|
||||
_QuestieQuest.objectiveSpawnListCallTable["item"] = item
|
||||
_QuestieQuest.objectiveSpawnListCallTable["spell"] = spell
|
||||
Reference in New Issue
Block a user