---@type QuestieLib local QuestieLib = QuestieLoader:ImportModule("QuestieLib") ---@type QuestieStream local QuestieStream = QuestieLoader:ImportModule("QuestieStreamLib") ---@type QuestieDB local QuestieDB = QuestieLoader:ImportModule("QuestieDB") ---@type QuestieOptions local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions") ---@type QuestieEventHandler local QuestieEventHandler = QuestieLoader:ImportModule("QuestieEventHandler") ---@type QuestieQuest local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest") ---@type QuestEventHandler local QuestEventHandler = QuestieLoader:ImportModule("QuestEventHandler") ---@class AvailableQuests local AvailableQuests = QuestieLoader:ImportModule("AvailableQuests") ---@type ZoneDB local ZoneDB = QuestieLoader:ImportModule("ZoneDB") ---@type MinimapIcon local MinimapIcon = QuestieLoader:ImportModule("MinimapIcon") ---@type TrackerLinePool local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool") ---@type QuestiePlayer local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer") ---@type QuestXP local QuestXP = QuestieLoader:ImportModule("QuestXP") ---@class QuestieCoords local QuestieCoords = QuestieLoader:ImportModule("QuestieCoords") ---@class Sounds local Sounds = QuestieLoader:ImportModule("Sounds") ---@class QuestieMenu local QuestieMenu = QuestieLoader:ImportModule("QuestieMenu") ---@class QuestieTooltips local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") ---@class QuestieNameplate local QuestieNameplate = QuestieLoader:ImportModule("QuestieNameplate") ---@type QuestieCorrections local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections") -- addon/folder name QuestieCompat.addonName = QuestieLoader.addonName or "Questie" -- polyfill hooksecurefunc for 1.12 if not hooksecurefunc then hooksecurefunc = function(t, k, f) if type(t) == "string" then f = k k = t t = _G end local old = t[k] if old then t[k] = function(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) local r1, r2, r3, r4, r5, r6, r7, r8, r9, r10 = old(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) return r1, r2, r3, r4, r5, r6, r7, r8, r9, r10 end else end end end -- polyfill math.mod for 3.3.5 / Retail (Lua 5.1+) if not math.mod then math.mod = math.fmod or function(a, b) return a - math.floor(a/b)*b end end QuestieCompat.NOOP = function() end QuestieCompat.NOOP_MT = { __index = function() return QuestieCompat.NOOP end } -- events handler QuestieCompat.frame = CreateFrame("Frame") QuestieCompat.frame:RegisterEvent("ADDON_LOADED") QuestieCompat.frame:RegisterEvent("PLAYER_LOGIN") QuestieCompat.frame:RegisterEvent("PLAYER_LOGOUT") QuestieCompat.frame:SetScript("OnEvent", function(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) QuestieCompat[event](self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) end) -- current expansion level (https://wowpedia.fandom.com/wiki/WOW_PROJECT_ID) QuestieCompat.WOW_PROJECT_CLASSIC = 2 QuestieCompat.WOW_PROJECT_BURNING_CRUSADE_CLASSIC = 5 QuestieCompat.WOW_PROJECT_WRATH_CLASSIC = 11 QuestieCompat.WOW_PROJECT_ID = tonumber(GetAddOnMetadata(QuestieCompat.addonName, "X-WOW_PROJECT_ID")) -- check for a specific type of group QuestieCompat.LE_PARTY_CATEGORY_HOME = 1 -- home-realm parties QuestieCompat.LE_PARTY_CATEGORY_INSTANCE = 2 -- instance-specific groups -- Date stuff QuestieCompat.CALENDAR_WEEKDAY_NAMES = { WEEKDAY_SUNDAY, WEEKDAY_MONDAY, WEEKDAY_TUESDAY, WEEKDAY_WEDNESDAY, WEEKDAY_THURSDAY, WEEKDAY_FRIDAY, WEEKDAY_SATURDAY, }; -- month names show up differently for full date displays in some languages QuestieCompat.CALENDAR_FULLDATE_MONTH_NAMES = { FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH, FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE, FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER, FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER, }; -- https://wago.tools/db2/ChrRaces?build=3.4.3.52237 QuestieCompat.ChrRaces = { Human = 1, Orc = 2, Dwarf = 3, NightElf = 4, Scourge = 5, Tauren = 6, Gnome = 7, Troll = 8, Goblin = 9, BloodElf = 10, Draenei = 11, FelOrc = 12, Naga_ = 13, Broken = 14, Skeleton = 15, Vrykul = 16, Tuskarr = 17, ForestTroll = 18, Taunka = 19, NorthrendSkeleton = 20, IceTroll = 21, } -- https://wago.tools/db2/ChrClasses?build=3.4.3.52237 QuestieCompat.ChrClasses = { WARRIOR = 1, PALADIN = 2, HUNTER = 3, ROGUE = 4, PRIEST = 5, DEATHKNIGHT = 6, SHAMAN = 7, MAGE = 8, WARLOCK = 9, DRUID = 11, } local activeTimers = {} local inactiveTimers = {} local function timerCancel(id) local timer = activeTimers[id] if not timer then return end timer:GetParent():Stop() timer.id = nil activeTimers[id] = nil inactiveTimers[timer] = true end local function timerOnFinished(self) local id = self.id self.callback(id) -- Make sure timer wasn't cancelled during the callback and used again if id == self.id then if self.iterations > 0 then self.iterations = self.iterations - 1 if self.iterations == 0 then timerCancel(id) end end end end QuestieCompat.C_Timer = { -- Schedules a (repeating) timer that can be canceled. (https://wowpedia.fandom.com/wiki/API_C_Timer.NewTimer) NewTicker = function(duration, callback, iterations) local timer = next(inactiveTimers) if timer then inactiveTimers[timer] = nil else local anim = QuestieCompat.frame:CreateAnimationGroup() timer = anim:CreateAnimation() timer:SetScript("OnFinished", timerOnFinished) end if duration < 0.01 then duration = 0.01 end timer:SetDuration(duration) timer.callback = callback timer.iterations = iterations or -1 timer.id = { Cancel = timerCancel } activeTimers[timer.id] = timer local anim = timer:GetParent() anim:SetLooping("REPEAT") anim:Play() return timer.id end, -- Schedules a timer. (https://wowpedia.fandom.com/wiki/API_C_Timer.After) After = function(duration, callback) return QuestieCompat.C_Timer.NewTicker(duration, callback, 1) end } local mapIdToUiMapId = {} -- convert current mapAreaID and mapLevel to UiMapId -- https://wowpedia.fandom.com/wiki/API_GetCurrentMapAreaID -- https://wowwiki-archive.fandom.com/wiki/API_GetCurrentMapDungeonLevel -- https://wowpedia.fandom.com/wiki/UiMapIDtable.getn(Classic) function QuestieCompat.GetCurrentUiMapID() local mapID = GetCurrentMapAreaID() if mapID == 0 then -- both the "Cosmic" and "Azeroth" maps return a mapID of 0 mapID = GetCurrentMapContinent() end return mapIdToUiMapId[mapID + GetCurrentMapDungeonLevel() / 10] or 946 end -- maps mapAreaID to Zone and Continent index -- https://wowpedia.fandom.com/wiki/API_GetMapContinents -- https://wowpedia.fandom.com/wiki/API_GetMapZones local mapIdToCZ = {} for C in ipairs({ GetMapContinents() }) do local zones = { GetMapZones(C) } for Z in ipairs(zones) do SetMapZoom(C, Z) local mapId = GetCurrentMapAreaID() mapIdToCZ[mapId] = Z + C / 10 end end function QuestieCompat.TomTom_AddWaypoint(title, zone, x, y) local CZ = mapIdToCZ[QuestieCompat.UiMapData[zone].mapID] return TomTom:AddZWaypoint(QuestieCompat.Round(math.mod(CZ, 1) * 10), math.floor(CZ), x, y, title) end -- This function will do its utmost to retrieve some sort of valid position -- for the player, including changing the current map zoom (if needed) -- https://wowpedia.fandom.com/wiki/API_C_Map.GetPlayerMapPosition?oldid=2167175 function QuestieCompat.GetCurrentPlayerPosition() local x, y = GetPlayerMapPosition("player"); if (x <= 0 and y <= 0) then if (WorldMapFrame:IsVisible()) then -- we know there is a visible world map, so don't cause -- WORLD_MAP_UPDATE events by changing map zoom return QuestieCompat.GetCurrentUiMapID(), x, y; end SetMapToCurrentZone(); x, y = GetPlayerMapPosition("player"); if (x <= 0 and y <= 0) then -- attempt to zoom out once - logic copied from WorldMapZoomOutButton_OnClick() if (ZoomOut()) then -- do nothing elseif (GetCurrentMapZone() ~= WORLDMAP_WORLD_ID) then SetMapZoom(GetCurrentMapContinent()); else SetMapZoom(WORLDMAP_WORLD_ID); end x, y = GetPlayerMapPosition("player"); if (x <= 0 and y <= 0) then -- we are in an instance without a map or otherwise off map return QuestieCompat.GetCurrentUiMapID(), x, y; end end end return QuestieCompat.GetCurrentUiMapID(), x, y; end -- wrapper used by QuestieCoords local playerPos = {} function QuestieCompat.GetPlayerMapPosition() playerPos.uiMapID, playerPos.x, playerPos.y = QuestieCompat.GetCurrentPlayerPosition() return playerPos, playerPos.uiMapID end QuestieCompat.C_Map = { GetPlayerMapPosition = function(uiMapID, unitToken) return QuestieCompat.GetPlayerMapPosition() end, -- Returns map information. -- https://wowpedia.fandom.com/wiki/API_C_Map.GetMapInfo GetMapInfo = function(uiMapID) if QuestieCompat.UiMapData[uiMapID] then return QuestieCompat.UiMapData[uiMapID] end end, -- Returns a map subzone name. -- https://wowpedia.fandom.com/wiki/API_C_Map.GetAreaInfo GetAreaInfo = function(areaID) return end, -- Returns the current UI map for the given unit. -- https://wowpedia.fandom.com/wiki/API_C_Map.GetBestMapForUnit GetBestMapForUnit = function(unit) if unit == "player" then return QuestieCompat.GetCurrentPlayerPosition() end end, -- Translates a map position to a world map position. -- https://wowpedia.fandom.com/wiki/API_C_Map.GetWorldPosFromMapPos GetWorldPosFromMapPos = function(uiMapID, mapPos) local x, y, instanceID = QuestieCompat.HBD:GetWorldCoordinatesFromZone(mapPos.x, mapPos.y, uiMapID) return instanceID or 0, { x = x or 0, y = y or 0 } end, -- Stub for GetMapChildrenInfo to prevent crashes on unsupported clients GetMapChildrenInfo = function(mapID, mapType, allDescendants) return {} end, } -- https://www.townlong-yak.com/framexml/classic/Blizzard_MapCanvas/Blizzard_MapCanvas.lua QuestieCompat.WorldMapFrame = { IsVisible = function(self) return WorldMapFrame:IsVisible() end, IsShown = function(self) return WorldMapFrame:IsShown() end, Show = function(self) ShowUIPanel(WorldMapFrame) end, GetCanvas = function(self) return WorldMapDetailFrame or WorldMapButton or WorldMapFrame end, GetMapID = QuestieCompat.GetCurrentUiMapID, SetMapID = function(self, UiMapID) local mapID = QuestieCompat.UiMapData[UiMapID].mapID local mapLevel = QuestieCompat.Round(math.mod(mapID, 1) * 10) SetMapByID(math.floor(mapID) - 1) if mapLevel > 0 then SetDungeonMapLevel(mapLevel) end end, EnumeratePinsByTemplate = function(self, template) return pairs(QuestieCompat.HBDPins.worldmapPins) end, } QuestieCompat.C_Calendar = { -- Returns information about the calendar month by offset. -- https://wowpedia.fandom.com/wiki/API_C_Calendar.GetMonthInfo GetMonthInfo = function(offsetMonths) local month, year, numdays, firstday = CalendarGetMonth(offsetMonth); return { month = month, year = year, numDays = numdays, firstWeekday = firstday, } end, } QuestieCompat.C_DateAndTime = { -- Returns the realm's current date and time. -- https://wowpedia.fandom.com/wiki/API_C_DateAndTime.GetCurrentCalendarTime GetCurrentCalendarTime = function() local weekday, month, day, year = CalendarGetDate(); local hours, minutes = GetGameTime() return { year = year, month = month, monthDay = day, weekday = weekday, hour = hours, minute = minutes } end } -- Returns the server's Unix time. -- https://wowpedia.fandom.com/wiki/API_GetServerTime function QuestieCompat.GetServerTime() local weekday, month, day, year = CalendarGetDate() local hours, minutes = GetGameTime() local currentDate = { year = year, month = month, day = day, weekday = weekday, hour = hours, min = minutes, } return time(currentDate), currentDate end local questObjectivesCache = {} local function parseQuestObjective(text) return string.match(string.gsub(text, "\239\188\154", ":"), "(.*):%s*([%d]+)%s*/%s*([%d]+)") end QuestieCompat.C_QuestLog = { GetQuestObjectives = function(questID, questLogIndex) local questObjectives = {} if questLogIndex then local numObjectives = GetNumQuestLeaderBoards(questLogIndex) for i = 1, numObjectives do local description, objectiveType, isCompleted = GetQuestLogLeaderBoard(i, questLogIndex) if objectiveType ~= "log" then local objectiveName, numFulfilled, numRequired = parseQuestObjective(description) local fulfilled = questObjectivesCache[objectiveName] if fulfilled then numFulfilled = fulfilled questObjectivesCache[objectiveName] = nil end table.insert(questObjectives, { text = description, type = objectiveType, finished = isCompleted and true or false, numFulfilled = tonumber(numFulfilled) or (isCompleted and 1 or 0), numRequired = tonumber(numRequired) or 1, }) end end end return questObjectives end, GetMaxNumQuestsCanAccept = function() return MAX_QUESTLOG_QUESTS end, IsOnQuest = function(questId) return QuestieCompat.GetQuestLogIndexByID(questId) ~= nil end, } -- Can't find anything about this function. -- Apparently, it returns true when quest data is ready to be queried. function QuestieCompat.HaveQuestData(questID) return true end -- https://wowpedia.fandom.com/wiki/API_GetQuestLogTitle?oldid=2214753 -- Returns information about a quest in your quest log. -- Patch 6.0.2 (2014-10-14): Removed returns 'questTag'. function QuestieCompat.GetQuestLogTitle(questLogIndex) local numReturns = select("#", GetQuestLogTitle(questLogIndex)) if numReturns == 0 then return nil end local ret = { GetQuestLogTitle(questLogIndex) } local questTitle = ret[1] local level = ret[2] local questTag = ret[3] local isHeader = ret[numReturns - 4] local isCollapsed = ret[numReturns - 3] local isComplete = ret[numReturns - 2] local isDaily = ret[numReturns - 1] local questID = ret[numReturns] if (isComplete == nil and not isHeader) then local numObjectives = GetNumQuestLeaderBoards(questLogIndex) local requiredMoney = GetQuestLogRequiredMoney(questLogIndex) isComplete = (numObjectives == 0 and GetMoney() >= requiredMoney) and 1 or nil end return questTitle, level, questTag, isHeader, isCollapsed, isComplete, isDaily and 2 or 1, questID end local MAX_QUEST_LOG_INDEX = 75 -- Returns the current quest log index of a quest by its ID. -- https://wowpedia.fandom.com/wiki/API_GetQuestLogIndexByID function QuestieCompat.GetQuestLogIndexByID(questId) local numEntries = select(1, GetNumQuestLogEntries()) or 0 for i = 1, numEntries do local title, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questID = GetQuestLogTitle(i) -- Do not break the loop because title=nil (you have a dummy entry #1) if title and not isHeader and questID and questID > 0 then if questID == questId then return i end end end return nil end function QuestieCompat.GetQuestIDFromLogIndex(questLogIndex) return select(8, QuestieCompat.GetQuestLogTitle(questLogIndex)) end -- https://wowpedia.fandom.com/wiki/API_GetQuestLink -- Returns a QuestLink for a quest. -- Between patches 6.2 and 7.3.2 argument was changed to take a QuestID instead of a quest log index. function QuestieCompat.GetQuestLink(questId) local questLogIndex = QuestieCompat.GetQuestLogIndexByID(questId) return questLogIndex and GetQuestLink(questLogIndex) end -- https://wowpedia.fandom.com/wiki/API_GetQuestLogRewardMoney -- Returns the amount of money rewarded for a quest. function QuestieCompat.GetQuestLogRewardMoney(questID) local rewardMoney = QuestieCompat.RewardMoney[questID] or 0 local rewardMoneyDifficulty = QuestieCompat.RewardMoneyDifficulty[questID] or 0 if rewardMoney < 0 then -- required money return rewardMoney end local playerLevel = QuestiePlayer.GetPlayerLevel() if playerLevel > 0 and rewardMoneyDifficulty > 0 then rewardMoney = QuestieCompat.QuestMoneyReward[playerLevel][rewardMoneyDifficulty] end -- https://wowpedia.fandom.com/wiki/Quest?oldid=1035002 Formula is XP gained * 6c if QuestiePlayer.IsMaxLevel() then local xpReward = QuestXP:GetQuestLogRewardXP(questID, true) if xpReward > 0 then rewardMoney = rewardMoney + xpReward * 6 end end return rewardMoney end function QuestieCompat.CalculateNextResetTime() local currentTime, currentDate = QuestieCompat.GetServerTime() local timeUntilReset = GetQuestResetTime() Questie:Debug(Questie.DEBUG_DEVELOP, "[CalculateNextResetTime] GetQuestResetTime: ", timeUntilReset) -- Some private/modified servers can return negative or zero values here. -- Instead of erroring out (and spamming chat), fall back to a sane default -- daily reset of 24 hours from now. if (not timeUntilReset) or (timeUntilReset <= 0) then Questie:Debug(Questie.DEBUG_DEVELOP, "[CalculateNextResetTime] Invalid GetQuestResetTime (" .. tostring(timeUntilReset) .. ") - using 24h fallback") timeUntilReset = 24 * 60 * 60 end Questie.db.profile.dailyResetTime = Questie.db.profile.dailyResetTime or (currentTime + timeUntilReset) Questie:Debug(Questie.DEBUG_DEVELOP, "[CalculateNextResetTime] Next daily rest time: ", date("%m/%d/%y %H:%M:%S", Questie.db.profile.dailyResetTime)) Questie.db.profile.weeklyResetHour = Questie.db.profile.weeklyResetHour or tonumber(date("%H", Questie.db.profile.dailyResetTime + 300)) local dayOffset = math.mod(Questie.db.profile.weeklyResetDay - currentDate.weekday + 7, 7) if dayOffset == 0 and currentDate.hour >= Questie.db.profile.weeklyResetHour then dayOffset = 7 end Questie.db.profile.weeklyResetTime = Questie.db.profile.weeklyResetTime or time({ year = currentDate.year, month = currentDate.month, day = currentDate.day + dayOffset, hour = Questie.db.profile.weeklyResetHour, }) Questie:Debug(Questie.DEBUG_DEVELOP, "[CalculateNextResetTime] Next weekly rest time: ", date("%m/%d/%y %H:%M:%S", Questie.db.profile.weeklyResetTime)) end function QuestieCompat.ResetDailyQuests(reset) local currentTime = QuestieCompat.GetServerTime() if reset or (currentTime > Questie.db.profile.dailyResetTime) then for questId in pairs(Questie.db.char.daily) do Questie.db.char.daily[questId] = nil Questie.db.char.complete[questId] = nil end Questie.db.profile.dailyResetTime = nil QuestieCompat.CalculateNextResetTime() if Questie.started then AvailableQuests.CalculateAndDrawAll() end end end local weeklyResetTimer function QuestieCompat.ResetWeeklyQuests() local currentTime = QuestieCompat.GetServerTime() local timeUntilReset = Questie.db.profile.weeklyResetTime - currentTime if timeUntilReset < 1800 then if weeklyResetTimer then weeklyResetTimer = weeklyResetTimer:Cancel() end weeklyResetTimer = weeklyResetTimer or QuestieCompat.C_Timer.After(timeUntilReset, function() for questId in pairs(Questie.db.char.weekly) do Questie.db.char.weekly[questId] = nil Questie.db.char.complete[questId] = nil end Questie.db.profile.weeklyResetTime = nil QuestieCompat.CalculateNextResetTime() if Questie.started then AvailableQuests.CalculateAndDrawAll() end end) return true end end function QuestieCompat.SetQuestComplete(questId) if (not QuestieDB.IsRepeatable(questId)) then Questie.db.char.complete[questId] = true end if Questie.db.profile.resetDailyQuests then if QuestieDB.IsDailyQuest(questId) then Questie.db.char.daily[questId] = true Questie.db.char.complete[questId] = true elseif QuestieDB.IsWeeklyQuest(questId) then Questie.db.char.weekly[questId] = true Questie.db.char.complete[questId] = true end end end -- Returns a list of quests the character has completed in its lifetime. -- https://wowpedia.fandom.com/wiki/API_GetQuestsCompleted function QuestieCompat.GetQuestsCompleted() if not Questie.db.char.complete then Questie.db.char.complete = {} end QueryQuestsCompleted() return Questie.db.char.complete end -- Fires when the data requested by QueryQuestsCompleted() is available. -- https://wowpedia.fandom.com/wiki/QUEST_QUERY_COMPLETE function QuestieCompat:QUEST_QUERY_COMPLETE(event) GetQuestsCompleted(Questie.db.char.complete) for questId in pairs(Questie.db.char.complete) do if QuestieDB.IsRepeatable(questId) then Questie.db.char.complete[questId] = nil end end if Questie.db.profile.resetDailyQuests then QuestieCompat.CalculateNextResetTime() QuestieCompat.ResetDailyQuests() QuestieCompat.Merge(Questie.db.char.complete, Questie.db.char.daily) if Questie.IsWotlk and QuestiePlayer.GetPlayerLevel() >= 78 then if (not QuestieCompat.ResetWeeklyQuests()) and (Questie.db.profile.weeklyResetDay == CalendarGetDate()) then weeklyResetTimer = weeklyResetTimer or QuestieCompat.C_Timer.NewTicker(1800, QuestieCompat.ResetWeeklyQuests) end QuestieCompat.Merge(Questie.db.char.complete, Questie.db.char.weekly) end end end -- https://wowpedia.fandom.com/wiki/API_IsQuestFlaggedCompleted -- Determine if a quest has been completed. function QuestieCompat.IsQuestFlaggedCompleted(questID) return Questie.db.char.complete[questID] or false end ---Returns the available quests at a quest giver. -- https://wowpedia.fandom.com/wiki/API_GetGossipAvailableQuests function QuestieCompat.GetAvailableQuests() local availableQuests = { GetGossipAvailableQuests() } local numAvailable = GetNumGossipAvailableQuests() for i = 1, numAvailable do local index = (i - 1) * 5 availableQuests[index + 3] = availableQuests[index + 3] and true or false availableQuests[index + 4] = availableQuests[index + 4] and 2 or 1 availableQuests[index + 5] = availableQuests[index + 5] and true or false end for i = 1, numAvailable do local index = (i - 1) * 7 table.insert(availableQuests, index + 6, false) table.insert(availableQuests, index + 7, false) end return unpack(availableQuests) end -- Returns the quests which can be turned in at a quest giver. -- https://wowpedia.fandom.com/wiki/API_GetGossipActiveQuests function QuestieCompat.GetActiveQuests() local activeQuests = { GetGossipActiveQuests() } local numActive = GetNumGossipActiveQuests() for i = 1, numActive do local index = (i - 1) * 4 activeQuests[index + 3] = activeQuests[index + 3] and true or false activeQuests[index + 4] = activeQuests[index + 4] and true or false end for i = 1, numActive do local index = (i - 1) * 6 table.insert(activeQuests, index + 5, false) table.insert(activeQuests, index + 6, false) end return unpack(activeQuests) end local questTagToName = { [1] = "Group", [41] = "PvP", [62] = "Raid", [81] = "Dungeon", [82] = "World Event", [83] = "Legendary", [84] = "Escort", [85] = "Heroic", } -- Retrieves tag information about the quest. -- https://wowpedia.fandom.com/wiki/API_GetQuestTagInfo function QuestieCompat.GetQuestTagInfo(questId) local tagId = QuestieCompat.QuestTag[questId] if tagId then return tagId, questTagToName[tagId] end end -- Returns the ID of the displayed quest at a quest giver. -- https://wowpedia.fandom.com/wiki/API_GetQuestID function QuestieCompat.GetQuestID(questStarter, title) local title = title or GetTitleText() local guid = QuestieCompat.UnitGUID("npc") return QuestieDB.GetQuestIDFromName(title, guid, questStarter) end function QuestieCompat.GetQuestIDFromName(questTitle) local numEntries = select(1, GetNumQuestLogEntries()) or 0 for questLogIndex = 1, numEntries do local title, _, _, _, isHeader, _, _, _, id = GetQuestLogTitle(questLogIndex) if title and (not isHeader) and id and id > 0 and title == questTitle then return id end end end -- https://wowwiki-archive.fandom.com/wiki/API_UnitGUID?oldid=2368080 local GUIDType = { [0] = "Player", [1] = "GameObject", [3] = "Creature", [4] = "Pet", [5] = "Vehicle" } -- Returns the GUID of the unit. -- https://wowpedia.fandom.com/wiki/GUID -- Patch 6.0.2 (2014-10-14): Changed to a new format function QuestieCompat.UnitGUID(unit) local guid = UnitGUID(unit) if guid then local math_mod = math.mod or math.fmod local type if math_mod then type = math_mod(tonumber(string.sub(guid, 5, 5), 16), 8) else local val = tonumber(string.sub(guid, 5, 5), 16) type = val - math.floor(val / 8) * 8 end if type and (type == 1 or type == 3 or type == 5) then local id = tonumber(string.sub(guid, 6, 12), 16) -- Creature-0-[serverID]-[instanceID]-[zoneUID]-[npcID]-[spawnUID] return string.format("%s-0-4170-0-41-%d-00000F4B37", GUIDType[type], id) end end end function QuestieCompat.GetMaxPlayerLevel() return (Questie.IsWotlk and 80) or (Questie.IsTBC and 70) or (Questie.IsClassic and 60) end -- https://wowpedia.fandom.com/wiki/API_UnitAura?oldid=2681338 -- Returns the buffs/debuffs for the unit. -- an alias for UnitAura(unit, index, "HELPFUL"), returning only buffs. -- Patch 8.0.1 (2018-07-17): Removed 'rank' return value. function QuestieCompat.UnitBuff(unit, index) local name, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, shouldConsolidate, spellId = UnitBuff(unit, index) return name, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, shouldConsolidate, spellId end -- Returns the race of the unit. -- https://wowpedia.fandom.com/wiki/API_UnitRace function QuestieCompat.UnitRace(unit) local raceName, raceFile = UnitRace(unit) return raceName, raceFile, QuestieCompat.ChrRaces[raceFile] end -- Returns the class of the unit. -- https://wowpedia.fandom.com/wiki/API_UnitClass -- Patch 5.0.4 (2012-08-28): Added classId return value. function QuestieCompat.UnitClass(unit) local className, classFile = UnitClass(unit) return className, classFile, QuestieCompat.ChrClasses[classFile] end -- Returns info for a faction. -- https://wowpedia.fandom.com/wiki/API_GetFactionInfo -- Patch 5.0.4 (2012-08-28): Added new return value: factionID -- TODO: localize factions name(https://www.curseforge.com/wow/addons/libbabble-faction-3-0) function QuestieCompat.GetFactionInfo(factionIndex) local name, description, standingId, bottomValue, topValue, earnedValue, atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(factionIndex) return name, description, standingId, bottomValue, topValue, earnedValue, atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild, QuestieCompat.FactionId[name:trim()] end -- Returns true if the unit is a member of your party -- https://wowpedia.fandom.com/wiki/API_UnitInParty -- As of 2.0.3, UnitInParty("player") always returns 1, even when you are not in a party. function QuestieCompat.UnitInParty(unit) if unit == "player" then return QuestieCompat.IsInGroup() end return UnitInParty(unit) end -- Returns true if the player is in a group. -- https://wowpedia.fandom.com/wiki/API_IsInGroup function QuestieCompat.IsInGroup(groupType) if groupType then return false end return UnitInParty("player") and GetNumPartyMembers() > 0 end -- Returns true if the player is in a raid. -- https://wowpedia.fandom.com/wiki/API_IsInRaid function QuestieCompat.IsInRaid(groupType) if groupType then return false end return UnitInRaid("player") and GetNumRaidMembers() > 0 end -- Returns names of characters in your home (non-instance) party. -- https://wowpedia.fandom.com/wiki/API_GetHomePartyInfo function QuestieCompat.GetHomePartyInfo(homePlayers) if QuestieCompat.UnitInParty("player") then homePlayers = homePlayers or {} for i = 1, MAX_PARTY_MEMBERS do if GetPartyMember(i) then table.insert(homePlayers, UnitName("party" .. i)) end end return homePlayers end end -- Gets a list of the auction house item classes. -- https://wowpedia.fandom.com/wiki/API_GetAuctionItemClasses?oldid=1835520 local itemClass = { GetAuctionItemClasses() } for classId, className in ipairs(itemClass) do itemClass[className] = classId itemClass[classId] = nil end -- Returns info for an item. -- https://wowpedia.fandom.com/wiki/API_GetItemInfo?oldid=2376031 -- Patch 7.0.3 (2016-07-19): Added classID, subclassID returns. function QuestieCompat.GetItemInfo(item) local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, itemSellPrice = GetItemInfo(item) return itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, itemSellPrice, itemClass[itemType] end -- Returns info for an item in a container slot. -- https://wowpedia.fandom.com/wiki/API_GetContainerItemInfo function QuestieCompat.GetContainerItemInfo(bagID, slot) local iconFile, stackCount, isLocked, quality, isReadable, hasLoot, hyperlink = GetContainerItemInfo(bagID, slot) if hyperlink then local itemID = string.match(hyperlink, "(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+)") -- GetContainerItemInfo does not return a quality value for all items. If it does not, it returns -1 if quality and quality < 0 then quality = (select(3, GetItemInfo(hyperlink))) end return iconFile, stackCount, isLocked, quality, isReadable, hasLoot, hyperlink, false, false, tonumber(itemID), false end end -- https://wowpedia.fandom.com/wiki/API_IsSpellKnown QuestieCompat.IsSpellKnownOrOverridesKnown = IsSpellKnown QuestieCompat.IsPlayerSpell = IsSpellKnown local LARGE_NUMBER_SEPERATOR = "."; function QuestieCompat.FormatLargeNumber(amount) amount = tostring(amount); local newDisplay = ""; local strlen = string.len(amount); --Add each thing behind a comma for i = 4, strlen, 3 do newDisplay = LARGE_NUMBER_SEPERATOR .. string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay; end --Add everything before the first comma newDisplay = string.sub(amount, 1, (math.mod(strlen, 3) == 0) and 3 or (math.mod(strlen, 3))) .. newDisplay; return newDisplay; end local function Round(value) if value < 0.0 then return math.ceil(value - .5); end return math.floor(value + .5); end QuestieCompat.Round = Round local function GenerateHexColor(r, g, b, a) return string.format(("ff%.2x%.2x%.2x"), Round(r * 255), Round(g * 255), Round(b * 255), Round((a or 1) * 255)); end -- Returns the color value associated with a given class. function QuestieCompat.GetClassColor(classFilename) local color = RAID_CLASS_COLORS[classFilename]; if color then return color.r, color.g, color.b, GenerateHexColor(color.r, color.g, color.b) end return 1, 1, 1, "ffffffff"; end -- handle tooltip based on the parent frame function QuestieCompat.SetupTooltip(frame, OnHide) if (frame:GetParent() == WorldMapFrame) then WorldMapPOIFrame.allowBlobTooltip = OnHide and true or false QuestieCompat.Tooltip = WorldMapTooltip else QuestieCompat.Tooltip = GameTooltip end return QuestieCompat.Tooltip end local wrappedLines = {} -- tooltip word wrapping looks fine the way it is, leave it for now function QuestieCompat.TextWrap(self, line, prefix, combineTrailing, desiredWidth) QuestieCompat.Tooltip:AddLine(line, 0.86, 0.86, 0.86, 1); return wrappedLines end local unboundedFS = UIParent:CreateFontString(nil, "ARTWORK", "GameFontNormal") unboundedFS:SetPoint("TOPLEFT", 0, 0) unboundedFS:Hide() -- The minimum width necessary to contain the entire text without truncation -- https://wowpedia.fandom.com/wiki/API_FontString_GetStringWidth function QuestieCompat.GetUnboundedStringWidth(self) unboundedFS:SetWidth(0) unboundedFS:SetFont(self:GetFont()) unboundedFS:SetText(self:GetText()) return unboundedFS:GetStringWidth() end -- Number of lines of wrapped text -- https://wowpedia.fandom.com/wiki/API_FontString_GetNumLines function QuestieCompat.GetNumLines(self) local fontName, fontHeight, fontFlags = self:GetFont() unboundedFS:SetWidth(self:GetWidth()) unboundedFS:SetFont(fontName, fontHeight, fontFlags) unboundedFS:SetText(self:GetText()) return math.ceil(unboundedFS:GetHeight() / fontHeight) end -- texture - Texture -- canvasFrame - Canvas Frame (for anchoring) -- startX,startY - Coordinate of start of line -- endX,endY - Coordinate of end of line -- lineWidth - Width of line -- relPoint - Relative point on canvas to interpret coords (Default BOTTOMLEFT) local function DrawLine(texture, canvasFrame, startX, startY, endX, endY, lineWidth, lineFactor, relPoint) if (not relPoint) then relPoint = "BOTTOMLEFT"; end lineFactor = lineFactor * .5; -- Determine dimensions and center point of line local dx, dy = endX - startX, endY - startY; local cx, cy = (startX + endX) / 2, (startY + endY) / 2; -- Normalize direction if necessary if (dx < 0) then dx, dy = -dx, -dy; end -- Calculate actual length of line local lineLength = sqrt((dx * dx) + (dy * dy)); -- Quick escape if it'sin zero length if (lineLength == 0) then texture:ClearAllPoints(); texture:SetTexCoord(0, 0, 0, 0, 0, 0, 0, 0); texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx, cy); texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx, cy); return; end -- Sin and Cosine of rotation, and combination (for later) local sin, cos = -dy / lineLength, dx / lineLength; local sinCos = sin * cos; -- Calculate bounding box size and texture coordinates local boundingWidth, boundingHeight, bottomLeftX, bottomLeftY, topLeftX, topLeftY, topRightX, topRightY, bottomRightX, bottomRightY; if (dy >= 0) then boundingWidth = ((lineLength * cos) - (lineWidth * sin)) * lineFactor; boundingHeight = ((lineWidth * cos) - (lineLength * sin)) * lineFactor; bottomLeftX = (lineWidth / lineLength) * sinCos; bottomLeftY = sin * sin; bottomRightY = (lineLength / lineWidth) * sinCos; bottomRightX = 1 - bottomLeftY; topLeftX = bottomLeftY; topLeftY = 1 - bottomRightY; topRightX = 1 - bottomLeftX; topRightY = bottomRightX; else boundingWidth = ((lineLength * cos) + (lineWidth * sin)) * lineFactor; boundingHeight = ((lineWidth * cos) + (lineLength * sin)) * lineFactor; bottomLeftX = sin * sin; bottomLeftY = -(lineLength / lineWidth) * sinCos; bottomRightX = 1 + (lineWidth / lineLength) * sinCos; bottomRightY = bottomLeftX; topLeftX = 1 - bottomRightX; topLeftY = 1 - bottomLeftX; topRightY = 1 - bottomLeftY; topRightX = topLeftY; end -- Set texture coordinates and anchors texture:ClearAllPoints(); texture:SetTexCoord(topLeftX, topLeftY, bottomLeftX, bottomLeftY, topRightX, topRightY, bottomRightX, bottomRightY); texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx - boundingWidth, cy - boundingHeight); texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx + boundingWidth, cy + boundingHeight); end -- Mix this into a Texture to be able to treat it like a line local LineMixin = {}; function LineMixin:SetStartPoint(relPoint, x, y) self.startX, self.startY = x, y; end function LineMixin:SetEndPoint(relPoint, x, y) self.endX, self.endY = x, y; end function LineMixin:SetThickness(thickness) self.thickness = thickness; end function LineMixin:Draw() local parent = self:GetParent(); local x, y = parent:GetLeft(), parent:GetBottom(); self:ClearAllPoints(); DrawLine(self, parent, self.startX - x, self.startY - y, self.endX - x, self.endY - y, self.thickness or 32, 1); end local function drawLineOnShow(self) local line = self.line DrawLine(line, self, line.startX, line.startY, line.endX, line.endY, line.thickness * 15, 1.2, "TOPLEFT"); end local stub_line = setmetatable({}, QuestieCompat.NOOP_MT) -- https://wowpedia.fandom.com/wiki/API_Frame_CreateLine function QuestieCompat.CreateLine(self) if self.line then return stub_line end -- stub lineBorder, as our line texture already has border local line = self:CreateTexture(nil, "OVERLAY") line:SetTexture(QuestieLib.AddonPath .. "Compat\\Icons\\Waypoint-Line.blp") line.SetColorTexture = line.SetVertexColor for k, v in pairs(LineMixin) do line[k] = v end self:SetScript("OnShow", drawLineOnShow) return line end QuestieCompat.LibUIDropDownMenu = { Create_UIDropDownMenu = function(self, name, parent) return CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate") end, EasyMenu = function(self, menuList, menuFrame, anchor, x, y, displayMode, autoHideDelay) EasyMenu(menuList, menuFrame, anchor, x, y, displayMode, autoHideDelay) end, CloseDropDownMenus = function(self, level) CloseDropDownMenus(level) end, } QuestieCompat.KButtons = { Add = function(self, templateName, templateType) local button = CreateFrame("Button", "Questie_WorldMapButton", WorldMapFrame) button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") if IsAddOnLoaded and IsAddOnLoaded("Mapster") then button:SetPoint("TOPRIGHT", WorldMapFrame, "TOPRIGHT", -50, -72.3) else button:SetPoint("TOPRIGHT", WorldMapFrame, "TOPRIGHT", -50, -40) end --button:SetPoint("TOPRIGHT", WorldMapFrame, "TOPRIGHT", -50, -70); button:SetFrameLevel(99) button:SetSize(32, 32) button:RegisterForClicks("anyUp") button:SetScript("OnMouseDown", QuestieWorldMapButtonMixin.OnMouseDown) button:SetScript("OnEnter", QuestieWorldMapButtonMixin.OnEnter) button:SetScript("OnLeave", function(self) QuestieCompat.SetupTooltip(self, true):Hide() end) local background = button:CreateTexture(nil, "BACKGROUND") background:SetSize(25, 25) background:SetTexture("Interface\\Minimap\\UI-Minimap-Background") background:SetPoint("TOPLEFT", 2, -4) local icon = button:CreateTexture(nil, "ARTWORK") icon:SetSize(20, 20) icon:SetTexture(QuestieLib.AddonPath .. "Icons\\complete.blp") icon:SetPoint("TOPLEFT", 6, -5) local border = button:CreateTexture(nil, "OVERLAY") border:SetSize(54, 54) border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") border:SetPoint("TOPLEFT") return button end, } --[[ xpcall wrapper implementation ]] local xpcall = xpcall local function errorhandler(err) return geterrorhandler()(err) end local method, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 local function call() return method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) end function QuestieCompat.xpcall(func, eh, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21, p22, p23, p24, p25) if type(func) == "function" then method = func a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21, p22, p23, p24, p25 return xpcall(call, eh or errorhandler) end end --[[ It seems that the table size is capped in 3.3.5, with a maximum of 524,288 entries. For instance, this code triggers an error message: 'memory allocation error: block too big. local t = {} for i=1, 524289 do t[i] = true end Spliting the table into multiple subtables should do the trick. ]] local stringchar = string.char local MAX_TABLE_SIZE = 524288 function QuestieCompat._writeByte(self, val) local subIndex = math.ceil(self._pointer / MAX_TABLE_SIZE) local index = self._pointer - (subIndex - 1) * MAX_TABLE_SIZE self._bin[subIndex] = self._bin[subIndex] or {} self._bin[subIndex][index] = stringchar(val) self._pointer = self._pointer + 1 end function QuestieCompat._readByte(self) local subIndex = math.ceil(self._pointer / MAX_TABLE_SIZE) local index = self._pointer - (subIndex - 1) * MAX_TABLE_SIZE self._pointer = self._pointer + 1 return self._bin[subIndex][index] end function QuestieCompat.Save(self) local result = "" for i = 1, table.getn(self._bin) do result = result .. table.concat(self._bin[i]) end return result end local _QuestieNameplate = QuestieNameplate.private local npFrames = {} local npActiveQuestNPCs = {} local npBorderTexture = "Interface\\Tooltips\\Nameplate-Border" local function isNamePlate(frame) if frame.UnitFrame -- ElvUI or frame.extended -- TidyPlates or frame.aloftData -- Aloft or frame.kui -- Kui_Nameplate then return true end local _, borderRegion = frame:GetRegions() if borderRegion and borderRegion:GetObjectType() == "Texture" then return borderRegion:GetTexture() == npBorderTexture end return false end local function scanWorldFrameChildren(frame, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) if not frame then return end if not npFrames[frame] and isNamePlate(frame) then npFrames[frame] = select(7, frame:GetRegions()) frame:HookScript("OnShow", QuestieCompat.NameplateCreated) frame:HookScript("OnHide", _QuestieNameplate.RemoveFrame) if frame:IsShown() then QuestieCompat.NameplateCreated(frame) end end return scanWorldFrameChildren(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) end function QuestieCompat.NameplateCreated(frame) if IsAddOnLoaded and IsAddOnLoaded("Ascension_NamePlates") then return end local name = npFrames[frame]:GetText() local key = npActiveQuestNPCs[name] if key then local icon = _QuestieNameplate.GetValidIcon(QuestieTooltips.lookupByKey[key]) if icon then local f = _QuestieNameplate.GetFrame(frame) f.Icon:SetTexture(icon) f.lastIcon = icon -- this is used to prevent updating the texture when it's already what it needs to be f:Show() end end end function QuestieCompat.UpdateNameplate() for frame in pairs(npFrames) do local name = npFrames[frame]:GetText() local key = npActiveQuestNPCs[name] local icon = _QuestieNameplate.GetValidIcon(QuestieTooltips.lookupByKey[key]) if icon then local f = _QuestieNameplate.GetFrame(frame) -- check if the texture needs to be changed if f.lastIcon ~= icon then f.lastIcon = icon f.Icon:SetTexture(icon) end else -- tooltip removed but we still have the frame active, remove it _QuestieNameplate.RemoveFrame(frame) end end end function QuestieCompat:QuestieTooltips_RegisterObjectiveTooltip(questId, key, objective) if string.find(key, "m_") then local name = QuestieDB.QueryNPCSingle(tonumber(string.sub(key, 3)), "name") npActiveQuestNPCs[name] = key end end local _EventHandler = QuestieEventHandler.private local chatMessagePattern = { questInfo = { ERR_QUEST_OBJECTIVE_COMPLETE_S, ERR_QUEST_UNKNOWN_COMPLETE, ERR_QUEST_ADD_KILL_SII, ERR_QUEST_ADD_FOUND_SII, ERR_QUEST_ADD_ITEM_SII, ERR_QUEST_ADD_PLAYER_KILL_SII, ERR_QUEST_FAILED_S, }, playerLoot = { LOOT_ITEM_CREATED_SELF, LOOT_ITEM_CREATED_SELF_MULTIPLE, LOOT_ITEM_PUSHED_SELF, LOOT_ITEM_PUSHED_SELF_MULTIPLE, LOOT_ITEM_SELF, LOOT_ITEM_SELF_MULTIPLE, } } -- parse chat message for quest related info function QuestieCompat.UiInfoMessage(event, message) for _, pattern in pairs(chatMessagePattern.questInfo) do if string.find(message, pattern) then local objectiveName, numFulfilled = parseQuestObjective(message) if objectiveName and numFulfilled then questObjectivesCache[objectiveName] = numFulfilled end MinimapIcon:UpdateText(message) end end end -- parse chat message for player looting an item local playerName = UnitName("player") local emptyName = "" function QuestieCompat.ChatMessageLoot(message) for _, pattern in pairs(chatMessagePattern.playerLoot) do if string.find(message, pattern) then return playerName end end return emptyName end -- handle remote questlog of the party/raid function QuestieCompat.GroupRosterUpdate(event) local currentMembers = QuestieCompat.IsInRaid() and GetNumRaidMembers() or GetNumPartyMembers() -- Only want to do logic when number increases, not decreases. if QuestiePlayer.numberOfGroupMembers < currentMembers then if QuestiePlayer.numberOfGroupMembers == 0 then _EventHandler:GroupJoined() end -- Tell comms to send information to members. --Questie:SendMessage("QC_ID_BROADCAST_FULL_QUESTLIST") QuestiePlayer.numberOfGroupMembers = currentMembers else if currentMembers == 0 then _EventHandler:GroupLeft() end -- We do however always want the local to be the current number to allow up and down. QuestiePlayer.numberOfGroupMembers = currentMembers end end function QuestieCompat.QuestieEventHandler_RegisterLateEvents() -- In fullscreen mode, WorldMap intercepts keyboard input, -- preventing the MODIFIER_STATE_CHANGED event if WorldMapFrame:GetScript("OnKeyDown") then local modifierStateChanged WorldMapFrame:HookScript("OnKeyDown", function(self, key) if IsModifierKeyDown() then _EventHandler:ModifierStateChanged(key, 1) modifierStateChanged = true end end) WorldMapFrame:HookScript("OnKeyUp", function(self, key) if modifierStateChanged then _EventHandler:ModifierStateChanged(key, 0) modifierStateChanged = nil end end) end Questie:UnregisterEvent("MAP_EXPLORATION_UPDATED") -- https://wowpedia.fandom.com/wiki/MAP_EXPLORATION_UPDATED Questie:UnregisterEvent("NEW_RECIPE_LEARNED") -- Party join event for QuestieComms, Use bucket to hinder this from spamming (Ex someone using a raid invite addon etc) Questie:UnregisterEvent("GROUP_ROSTER_UPDATE") -- https://wowpedia.fandom.com/wiki/GROUP_ROSTER_UPDATE Questie:UnregisterEvent("GROUP_JOINED") -- https://wowpedia.fandom.com/wiki/GROUP_JOINED Questie:UnregisterEvent("GROUP_LEFT") -- https://wowpedia.fandom.com/wiki/GROUP_LEFT Questie:RegisterEvent("PARTY_MEMBERS_CHANGED", QuestieCompat.GroupRosterUpdate) Questie:RegisterBucketEvent("RAID_ROSTER_UPDATE", 1, QuestieCompat.GroupRosterUpdate) -- Nameplate / Target Frame Objective Events Questie:UnregisterEvent("NAME_PLATE_UNIT_ADDED") -- https://wowpedia.fandom.com/wiki/NAME_PLATE_UNIT_ADDED Questie:UnregisterEvent("NAME_PLATE_UNIT_REMOVED") -- https://wowpedia.fandom.com/wiki/NAME_PLATE_UNIT_REMOVED if Questie.db.profile.nameplateEnabled then QuestieNameplate.UpdateNameplate = QuestieCompat.UpdateNameplate hooksecurefunc(QuestieQuest, "GetAllQuestIds", QuestieCompat.UpdateNameplate) hooksecurefunc(QuestieTooltips, "RegisterObjectiveTooltip", QuestieCompat.QuestieTooltips_RegisterObjectiveTooltip) local lastNumChildren QuestieCompat.C_Timer.NewTicker(0.1, function() local numChildren = WorldFrame:GetNumChildren() if numChildren ~= lastNumChildren then lastNumChildren = numChildren scanWorldFrameChildren(WorldFrame:GetChildren()) end end) end end local _QuestEventHandler = QuestEventHandler.private local QUEST_COMPLETE_MSG = string.gsub(ERR_QUEST_COMPLETE_S, "(%%s)", "(.+)") local completeQuestCache = {} local DAILY_QUESTS_MSG = string.gsub(DAILY_QUESTS_REMAINING, "%%d", "(%%d+)"):gsub("|4(.-)$", "") function QuestieCompat:CHAT_MSG_SYSTEM(event, message) local questName = string.match(message, QUEST_COMPLETE_MSG) if questName then local questId = completeQuestCache[questName] or QuestieCompat.GetQuestIDFromName(questName) if questId and questId > 0 then completeQuestCache[questName] = nil -- Ensure QuestEventHandler questLog state stays correct on auto turn-in _QuestEventHandler:QuestTurnedIn(questId) -- Tiny delay helps if the server removes the quest log entry instantly QuestieCompat.C_Timer.After(0.1, function() _QuestEventHandler:QuestRemoved(questId) if QuestieTracker and QuestieTracker.Update then QuestieTracker:Update() end end) end end if Questie.db.profile.resetDailyQuests then local dailyQuestCount = tonumber(string.match(message, DAILY_QUESTS_MSG)) if dailyQuestCount and (dailyQuestCount == GetMaxDailyQuests()) then QuestieCompat.C_Timer.After(1, function() QuestieCompat.ResetDailyQuests(true) end) end end end function QuestieCompat.QuestEventHandler_RegisterEvents() QuestieCompat.frame:RegisterEvent("QUEST_QUERY_COMPLETE") QuestieCompat.frame:RegisterEvent("CHAT_MSG_SYSTEM") -- https://wowpedia.fandom.com/wiki/PLAYER_INTERACTION_MANAGER_FRAME_HIDE QuestieQuestEventFrame:UnregisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE") for _, event in pairs({ "TRADE_CLOSED", "MERCHANT_CLOSED", "BANKFRAME_CLOSED", "GUILDBANKFRAME_CLOSED", "VENDOR_CLOSED", "MAIL_CLOSED", "AUCTION_HOUSE_CLOSED", }) do QuestieCompat.frame:RegisterEvent(event) QuestieCompat[event] = _QuestEventHandler.QuestRelatedFrameClosed end -- https://wowpedia.fandom.com/wiki/QUEST_TURNED_IN QuestieQuestEventFrame:UnregisterEvent("QUEST_TURNED_IN") hooksecurefunc("GetQuestReward", function(itemChoice) -- FIX: Added InCombatLockdown guard to prevent tainting secure execution paths. if InCombatLockdown() then return end local questTitle = GetTitleText() local questId = QuestieCompat.GetQuestIDFromName(questTitle) if questId and questId > 0 then completeQuestCache[questTitle] = questId end end) hooksecurefunc("SetAbandonQuest", function() -- FIX: Added InCombatLockdown guard to prevent tainting secure execution paths. if InCombatLockdown() then return end QuestieCompat.abandonQuestID = QuestieCompat.GetQuestIDFromLogIndex(GetQuestLogSelection()) end) --https://wowpedia.fandom.com/wiki/QUEST_REMOVED QuestieQuestEventFrame:UnregisterEvent("QUEST_REMOVED") hooksecurefunc("AbandonQuest", function() -- FIX: Added InCombatLockdown guard and pcall to prevent tainting secure execution paths. if InCombatLockdown() then return end local questId = QuestieCompat.abandonQuestID or QuestieCompat.GetQuestIDFromLogIndex(GetQuestLogSelection()) QuestieCompat.abandonQuestID = nil if questId and questId > 0 then pcall(_QuestEventHandler.QuestRemoved, _QuestEventHandler, questId) end end) end function QuestieCompat.QuestieTracker_Initialize(trackerQuestFrame) -- TrackerHeaderFrame.Initialize Questie_HeaderFrame.trackedQuests.label.GetUnboundedStringWidth = QuestieCompat.GetUnboundedStringWidth -- TrackerQuestFrame.Initialize trackerQuestFrame.ScrollFrame.scrollBarHideable = true trackerQuestFrame.ScrollBar:ClearAllPoints() trackerQuestFrame.ScrollBar:SetPoint("TOPRIGHT", trackerQuestFrame.ScrollUpButton, "BOTTOMRIGHT", -1, 4) trackerQuestFrame.ScrollBar:SetPoint("BOTTOMRIGHT", trackerQuestFrame.ScrollDownButton, "TOPRIGHT", -1, -4) trackerQuestFrame.ScrollDownButton:SetPoint("BOTTOMRIGHT", trackerQuestFrame.ScrollFrame, "BOTTOMRIGHT", -4, 12) trackerQuestFrame.ScrollBg:SetTexture(0, 0, 0, 0.35) trackerQuestFrame.ScrollBg:Show() trackerQuestFrame.ScrollBar.Show = function() end -- TrackerLinePool.Initialize for i = 1, 250 do local line = _G["linePool" .. i] line.label.GetUnboundedStringWidth = QuestieCompat.GetUnboundedStringWidth line.label.GetWrappedWidth = line.label.GetWidth line.label.GetNumLines = QuestieCompat.GetNumLines end end -- prevents the override of existing global variables with the same name(e.g., WorldMapButton) function QuestieCompat.PopulateGlobals(self) for name, module in pairs(QuestieLoader._modules) do if not _G[name] then _G[name] = module end end end -- change sound files extension from .ogg to .wav function QuestieCompat.GetSelectedSoundFile(typeSelected) return QuestieCompat.orig_GetSelectedSoundFile(typeSelected):gsub("[^.]+$", "wav") end -- disable builtin quest progress tooltips, re-enable on logout function QuestieCompat:ToggleQuestTrackingTooltips(event) local value = tostring(string.find(event, "LOGOUT") and 1 or 0) SetCVar("showQuestTrackingTooltips", value) end QuestieCompat.PLAYER_LOGIN = QuestieCompat.ToggleQuestTrackingTooltips QuestieCompat.PLAYER_LOGOUT = QuestieCompat.ToggleQuestTrackingTooltips local townsfolk_texturemap = { ["Ammo"] = "Interface\\Icons\\inv_ammo_arrow_02", ["Bags"] = "Interface\\Icons\\inv_misc_bag_09", ["Potions"] = "Interface\\Icons\\inv_potion_51", ["Trade Goods"] = "Interface\\Icons\\inv_fabric_wool_02", ["Drink"] = "Interface\\Icons\\inv_potion_01", ["Food"] = "Interface\\Icons\\inv_misc_food_11", ["Pet Food"] = "Interface\\Icons\\ability_hunter_beasttraining", ["Spirit Healer"] = "Interface\\Addons\\" .. QuestieCompat.addonName .. "\\Compat\\Icons\\Raid-Icon-Rez.blp", ["Portal Trainer"] = "Interface\\Addons\\" .. QuestieCompat.addonName .. "\\Compat\\Icons\\Vehicle-AllianceMagePortal.blp", } StaticPopupDialogs["QUESTIE_RELOAD"] = { text = "Changes you have made require a UI reload", button1 = 'Reload UI', button2 = CANCEL, OnAccept = function() ReloadUI() end, OnShow = function(self) self:SetFrameStrata("TOOLTIP") end, timeout = 0, whileDead = true, hideOnEscape = true, preferredIndex = 3 } function QuestieCompat.QuestieOptions_Initialize() QuestieCompat.orig_QuestieOptions_Initialize() local optionsTable = LibStub("AceConfigRegistry-3.0"):GetOptionsTable("Questie", "dialog", "MyLib-1.0") -- revert instant quest text to old cvar optionsTable.args.general_tab.args.interface_options_group.args.instantQuest.get = function() return GetCVar("questFadingDisable") == '1' and true or false end optionsTable.args.general_tab.args.interface_options_group.args.instantQuest.set = function(info, value) QUEST_FADING_DISABLE = tostring(value and 1 or 0) SetCVar("questFadingDisable", tostring(value and 1 or 0)) end optionsTable.args.nameplate_tab.args.nameplate_options_group.args.nameplateEnabled.set = function(info, value) QuestieOptions:SetProfileValue(info, value) StaticPopup_Show("QUESTIE_RELOAD") end -- disable settings for not implemented functionality Questie.db.profile.hideUnexploredMapIcons = false optionsTable.args.icons_tab.args.map_settings_group.args.hideUnexploredMapIconsToggle.disabled = true -- 3.3.5 section optionsTable.args.advanced_tab.args.compat_header = { type = "header", order = 6, name = "3.3.5 Compatibility Settings", } optionsTable.args.advanced_tab.args.useWotlkMapData = { type = "toggle", order = 6.1, name = "Use WotLK map data", desc = "Use WotLK map data", width = 1.65, disabled = function() return QuestieCompat.WOW_PROJECT_ID == QuestieCompat.WOW_PROJECT_WRATH_CLASSIC end, get = function(info) return QuestieOptions:GetProfileValue(info); end, set = function(info, value) QuestieOptions:SetProfileValue(info, value) StaticPopup_Show("QUESTIE_RELOAD") end, } optionsTable.args.advanced_tab.args.initDelay = { type = "range", order = 6.2, name = "Init rate delay", desc = "Init rate delay", width = 1.65, min = 0.1, max = 1, step = 0.01, hidden = function() return not Questie.db.profile.debugEnabled; end, get = function(info) return QuestieOptions:GetProfileValue(info) * 10; end, set = function(info, value) QuestieOptions:SetProfileValue(info, value / 10) end, } optionsTable.args.advanced_tab.args.resetDailyQuests = { type = "toggle", order = 6.2, name = "Reset Daily Quests", desc = "Reset Daily Quests", width = 1.65, get = function(info) return QuestieOptions:GetProfileValue(info); end, set = function(info, value) QuestieOptions:SetProfileValue(info, value) Questie.db.profile.dailyResetTime = nil StaticPopup_Show("QUESTIE_RELOAD") end, } optionsTable.args.advanced_tab.args.weeklyResetDay = { type = "select", order = 6.3, values = QuestieCompat.CALENDAR_WEEKDAY_NAMES, style = 'dropdown', disabled = function() return not Questie.db.profile.resetDailyQuests end, name = "Weekly Reset Day", desc = "Weekly Reset Day", width = 1.6, get = function(info) return QuestieOptions:GetProfileValue(info) end, set = function(info, value) QuestieOptions:SetProfileValue(info, value) Questie.db.profile.weeklyResetTime = nil StaticPopup_Show("QUESTIE_RELOAD") end, } end local correctionsRegistry = {} function QuestieCompat.RegisterCorrection(dbName, corrections) correctionsRegistry[dbName] = correctionsRegistry[dbName] or {} table.insert(correctionsRegistry[dbName], corrections) end function QuestieCompat.LoadCorrections(_LoadCorrections, validationTables) for dbName in pairs(correctionsRegistry) do local dbKeysReversed = QuestieDB[string.sub(dbName, 1, -5) .. "KeysReversed"] for i, corrections in ipairs(correctionsRegistry[dbName]) do _LoadCorrections(dbName, corrections(), dbKeysReversed, validationTables) end end end local blacklistRegistry = {} function QuestieCompat.RegisterBlacklist(blName, blacklist) blacklistRegistry[blName] = blacklistRegistry[blName] or {} table.insert(blacklistRegistry[blName], blacklist) end function QuestieCompat.LoadBlacklists() for blName in pairs(blacklistRegistry) do for _, blacklist in ipairs(blacklistRegistry[blName]) do QuestieCompat.Merge(QuestieCorrections[blName], blacklist(), true) end end end function QuestieCompat.Merge(target, source, override) if type(target) ~= "table" then target = {} end for k, v in pairs(source) do if type(v) == "table" then target[k] = QuestieCompat.Merge(target[k], v, override) elseif target[k] == nil or override then target[k] = v end end return target end function QuestieCompat:ADDON_LOADED(event, addon) if addon ~= QuestieCompat.addonName then return end QuestieCompat.Merge(Questie.db, { profile = { initDelay = 0.03, useWotlkMapData = false, resetDailyQuests = true, weeklyResetDay = 4, }, char = { daily = {}, weekly = {}, } }) QuestieCompat.LoadUiMapData(Questie.db.profile.useWotlkMapData and QuestieCompat.WOW_PROJECT_WRATH_CLASSIC) for uiMapId, data in pairs(QuestieCompat.UiMapData) do mapIdToUiMapId[data.mapID] = uiMapId end for k, patterns in pairs(chatMessagePattern) do for i, str in pairs(patterns) do chatMessagePattern[k][i] = QuestieLib:SanitizePattern(str) end end for name, path in pairs(townsfolk_texturemap) do QuestieMenu.private.townsfolk_texturemap[name] = path end for _, moduleName in pairs({ "HBDHooks", "QuestieDebugOffer", "SeasonOfDiscovery", "QuestieDBMIntegration", }) do local module = QuestieLoader:ImportModule(moduleName) setmetatable(module, QuestieCompat.NOOP_MT) end QuestieLoader.PopulateGlobals = QuestieCompat.PopulateGlobals QuestieStream._writeByte = QuestieCompat._writeByte QuestieStream._readByte = QuestieCompat._readByte QuestieStream.Save = QuestieCompat.Save ZoneDB.private.RunTests = QuestieCompat.NOOP QuestieLib.TextWrap = QuestieCompat.TextWrap QuestieCoords.GetPlayerMapPosition = QuestieCompat.GetPlayerMapPosition QuestieCoords.ResetMiniWorldMapText = QuestieCompat.NOOP _EventHandler.UiInfoMessage = QuestieCompat.UiInfoMessage QuestieCompat.orig_QuestieOptions_Initialize = QuestieOptions.Initialize QuestieOptions.Initialize = QuestieCompat.QuestieOptions_Initialize QuestieCompat.orig_GetSelectedSoundFile = Sounds.GetSelectedSoundFile Sounds.GetSelectedSoundFile = QuestieCompat.GetSelectedSoundFile hooksecurefunc(QuestieEventHandler, "RegisterLateEvents", QuestieCompat.QuestieEventHandler_RegisterLateEvents) hooksecurefunc(QuestEventHandler, "RegisterEvents", QuestieCompat.QuestEventHandler_RegisterEvents) hooksecurefunc(TrackerLinePool, "Initialize", QuestieCompat.QuestieTracker_Initialize) hooksecurefunc(QuestieQuest, "ToggleNotes", QuestieCompat.HBDPins.UpdateWorldMap) local Mapster = LibStub("AceAddon-3.0"):GetAddon("Mapster", true) if Mapster and Mapster.RefreshQuestObjectivesDisplay then hooksecurefunc(Mapster, "RefreshQuestObjectivesDisplay", QuestieCompat.HBDPins.UpdateWorldMap) end local MBF = LibStub("AceAddon-3.0"):GetAddon("Minimap Button Frame", true) if MBF and MBF.db.profile.MinimapIcons then table.insert(MBF.db.profile.MinimapIcons, "QuestieFrame") MBF:fillDropdowns() end end