---@class QuestieComms local QuestieComms = QuestieLoader:CreateModule("QuestieComms"); local _QuestieComms = QuestieComms.private ------------------------- --Import modules. ------------------------- ---@type QuestieSerializer local QuestieSerializer = QuestieLoader:ImportModule("QuestieSerializer"); ---@type QuestieLib local QuestieLib = QuestieLoader:ImportModule("QuestieLib"); ---@type QuestiePlayer local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer"); ---@type QuestieDB local QuestieDB = QuestieLoader:ImportModule("QuestieDB"); ---@type DailyQuests local DailyQuests = QuestieLoader:ImportModule("DailyQuests"); ---@type ZoneDB local ZoneDB = QuestieLoader:ImportModule("ZoneDB") ---@type l10n local l10n = QuestieLoader:ImportModule("l10n") ---@type QuestLogCache local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") --- COMPATIBILITY --- local C_Timer = QuestieCompat.C_Timer local C_Map = QuestieCompat.C_Map local UnitInParty = QuestieCompat.UnitInParty local IsInRaid = QuestieCompat.IsInRaid local HBD = QuestieCompat.HBD or LibStub("HereBeDragonsQuestie-2.0") -- Addon message prefix _QuestieComms.prefix = "questie"; -- List of all players questlog private to prevent modification from the outside. QuestieComms.remoteQuestLogs = {} QuestieComms.remotePlayerClasses = {} QuestieComms.remotePlayerEnabled = {} QuestieComms.remotePlayerTimes = {} -- The idea here is that all messages with the same "base number" are compatible -- New message versions increase the number by 0.1, and if the message becomes "incompatible" you increase with 1 -- Say if the message is 1.5 it is valid as long as it is < 2. If it is 2.5 it is not compatible for example. local commMessageVersion = 5.0; local warnedUpdate = false; local suggestUpdate = true; -- forward declaration local _DoYell --Not used, contains a list of hashes for quest, used to compare change. --_QuestieComms.questHashes = {}; --Channel types _QuestieComms.QC_WRITE_ALLGUILD = "GUILD" _QuestieComms.QC_WRITE_ALLGROUP = "PARTY" _QuestieComms.QC_WRITE_ALLINSTANCE = "INSTANCE_CHAT" _QuestieComms.QC_WRITE_ALLRAID = "RAID" _QuestieComms.QC_WRITE_WHISPER = "WHISPER" _QuestieComms.QC_WRITE_CHANNEL = "CHANNEL" _QuestieComms.QC_WRITE_YELL = "YELL" --Message types. _QuestieComms.QC_ID_BROADCAST_QUEST_UPDATE = 1 -- send quest_log_update status to party/raid members _QuestieComms.QC_ID_BROADCAST_QUEST_REMOVE = 2 -- send quest remove status to party/raid members -- NYI -> --[[ _QuestieComms.QC_ID_BROADCAST_QUESTIE_GETVERSION = 2 -- ask anyone for the newest questie version _QuestieComms.QC_ID_BROADCAST_QUESTIE_VERSION = 3 -- broadcast the current questie version _QuestieComms.QC_ID_ASK_CHANGELOG = 4 -- ask a player for the changelog _QuestieComms.QC_ID_SEND_CHANGELOG = 5 _QuestieComms.QC_ID_ASK_QUESTS = 6 -- ask a player for their quest progress on specific quests _QuestieComms.QC_ID_SEND_QUESTS = 7 _QuestieComms.QC_ID_ASK_QUESTSLIST = 8 -- ask a player for their current quest log as a list of quest IDs _QuestieComms.QC_ID_SEND_QUESTSLIST = 9 quest.objectives[objectiveIndex].id = questObject.Objectives[objectiveIndex].Id; quest.objectives[objectiveIndex].typ = objective.type; quest.objectives[objectiveIndex].fin = objective.finished; quest.objectives[objectiveIndex].ful = objective.numFulfilled; quest.objectives[objectiveIndex].req = objective.numRequired; ]]-- -- <-- NYI _QuestieComms.QC_ID_BROADCAST_FULL_QUESTLIST = 10 _QuestieComms.QC_ID_REQUEST_FULL_QUESTLIST = 11 _QuestieComms.QC_ID_BROADCAST_FULL_QUESTLISTV2 = 12 _QuestieComms.QC_ID_YELL_PROGRESS = 13 _QuestieComms.QC_ID_BROADCAST_QUEST_UPDATEV2 = 14 -- v2 (future use) -- NOT USED -- stringLookup it built from idLookup! _QuestieComms.stringLookup = {} _QuestieComms.idLookup = { ["id"] = 1, ["type"] = 2, ["finished"] = 3, ["fulfilled"] = 4, ["required"] = 5, } for string, int in pairs(_QuestieComms.idLookup) do _QuestieComms.stringLookup[int] = string; end -- !NOT USED local badYellLocations = { -- Alliance [1453] = true, -- Stormwind [1455] = true, -- Ironforge [1457] = true, -- Darnassus [1947] = true, -- Exodar -- Horde [1454] = true, -- Orgrimmar [1456] = true, -- Thunder Bluff [1458] = true, -- Undercity [1954] = true, -- Silvermoon -- Both [1955] = true, -- Shattrath -- Battlegrounds [1459] = true, -- Alterac Valley [1460] = true, -- Warsong Gulch [1461] = true, -- Arathi Basin [1957] = true, -- Eye of the Storm } --- Global Functions -- --------- -- Fetch quest information about a specific player. -- Params: -- questId (int) -- playerName (string) OPTIONAL -- Return: -- Similar object as QuestieQuest:GetRawLeaderBoardDetails() -- Quest.(Id|level|isComplete) --title is trimmed to save space -- Quest.Objectives[index].(description|objectiveType|isCompleted) --------- --Only questid gets all players with that quest and their progress --Both name and questid returns a specific players progress if one exist. function QuestieComms:GetQuest(questId, playerName) if(QuestieComms.remoteQuestLogs[questId]) then if(playerName) then if(QuestieComms.remoteQuestLogs[questId][playerName]) then -- Create a copy of the object, other side should never be able to edit the underlying object. local quest = {}; for key, value in pairs(QuestieComms.remoteQuestLogs[questId][playerName]) do quest[key] = value; end return quest; end else -- Create a copy of the object, other side should never be able to edit the underlying object. local quest = {}; for pName, objectivesData in pairs(QuestieComms.remoteQuestLogs[questId]) do quest[pName] = objectivesData; end return quest; end end return nil; end function QuestieComms:Initialize() -- Lets us send any length of message. Also implements ChatThrottleLib to not get disconnected. Questie:RegisterComm(_QuestieComms.prefix, _QuestieComms.OnCommReceived); -- TODO: replace with getting data over own comms in properly throttled manner -- see: https://github.com/Questie/Questie/issues/3540 Questie:RegisterComm("REPUTABLE", DailyQuests.FilterDailies); -- Events to be used to broadcast updates to other people Questie:RegisterMessage("QC_ID_BROADCAST_QUEST_UPDATE", _QuestieComms.BroadcastQuestUpdate); Questie:RegisterMessage("QC_ID_BROADCAST_QUEST_REMOVE", _QuestieComms.BroadcastQuestRemove); -- Bucket for 2 seconds to prevent spamming. Questie:RegisterBucketMessage("QC_ID_BROADCAST_FULL_QUESTLIST", 2, _QuestieComms.BroadcastQuestLog); -- Responds to the "hi" event from others. Questie:RegisterMessage("QC_ID_REQUEST_FULL_QUESTLIST", _QuestieComms.RequestQuestLog); -- Part of yellcomms, removing -- if not Questie.db.profile.disableYellComms then -- C_Timer.NewTicker(60, QuestieComms.SortRemotePlayers) -- periodically check for old players and remove them. -- end end -- Local Functions -- function _QuestieComms:BroadcastQuestUpdate(questId) -- broadcast quest update to group or raid Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms:BroadcastQuestUpdate] Questid", questId) if(questId) then local partyType = QuestiePlayer:GetGroupType() Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms:BroadcastQuestUpdate] partyType", tostring(partyType)) if partyType then if partyType ~= "raid" then QuestieComms:YellProgress(questId) end --Do we really need to make this? local questPacket = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_BROADCAST_QUEST_UPDATE) local quest = QuestieComms:CreateQuestDataPacket(questId); questPacket.data.quest = quest questPacket.data.priority = "NORMAL"; if partyType == "raid" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLRAID elseif partyType == "instance" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLINSTANCE else questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLGROUP end questPacket:write() else QuestieComms:YellProgress(questId) end end end -- Removes the quest from everyones external quest-log function _QuestieComms:BroadcastQuestRemove(questId) -- broadcast quest update to group or raid local partyType = QuestiePlayer:GetGroupType() Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieComms:BroadcastQuestRemove] QuestId:", questId, "partyType:", tostring(partyType)) if partyType then --Do we really need to make this? local questPacket = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_BROADCAST_QUEST_REMOVE); questPacket.data.id = questId; --This is important! questPacket.data.priority = "ALERT"; if partyType == "raid" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLRAID; elseif partyType == "instance" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLINSTANCE else questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLGROUP; end questPacket:write(); end end local _classToIndex = { ["DRUID"] = 1, ["HUNTER"] = 2, ["MAGE"] = 3, ["PRIEST"] = 4, ["ROGUE"] = 5, ["SHAMAN"] = 6, ["WARLOCK"] = 7, ["WARRIOR"] = 8 } local _indexToClass = {} for class, index in pairs(_classToIndex) do _indexToClass[index] = class end function QuestieComms:PopulateQuestDataPacketV2_noclass_renameme(questId, quest, offset) local questObject = QuestieDB.GetQuest(questId); local count = 0 local rawObjectives = QuestLogCache.GetQuestObjectives(questId) -- DO NOT MODIFY THE RETURNED TABLE if (not rawObjectives) then return offset, count end if questObject and next(questObject.Objectives) then quest[offset] = questId local countOffset = offset+1 offset = offset + 2 for objectiveIndex, objective in pairs(rawObjectives) do -- DO NOT MODIFY THE RETURNED TABLE quest[offset] = questObject.Objectives[objectiveIndex].Id quest[offset + 1] = string.byte(string.sub(objective.type, 1, 1)) quest[offset + 2] = objective.numFulfilled quest[offset + 3] = objective.numRequired offset = offset + 4 count = count + 1 end quest[countOffset] = count end Questie:Debug(Questie.DEBUG_SPAM, "[QuestieComms] questPacket made: Objectivetable:", quest) return offset, count end function QuestieComms:PopulateQuestDataPacketV2(questId, quest, offset) local questObject = QuestieDB.GetQuest(questId); local count = 0 local rawObjectives = QuestLogCache.GetQuestObjectives(questId) -- DO NOT MODIFY THE RETURNED TABLE if (not rawObjectives) then return offset, count end if questObject and next(questObject.Objectives) then quest[offset] = questId local countOffset = offset+1 local _, classFilename = UnitClass("player") quest[offset+2] = _classToIndex[classFilename] offset = offset + 3 for objectiveIndex, objective in pairs(rawObjectives) do -- DO NOT MODIFY THE RETURNED TABLE quest[offset] = questObject.Objectives[objectiveIndex].Id quest[offset + 1] = string.byte(string.sub(objective.type, 1, 1)) quest[offset + 2] = objective.numFulfilled quest[offset + 3] = objective.numRequired offset = offset + 4 count = count + 1 end quest[countOffset] = count end Questie:Debug(Questie.DEBUG_SPAM, "[QuestieComms] questPacket made: Objectivetable:", quest) return offset, count end -- temporary function: refactor in 6.0.1 function QuestieComms:InsertQuestDataPacketV2_noclass_RenameMe(questPacket, playerName, offset, disableCompleteQuests) --We don't want to insert our own quest data. local allDone = true if questPacket then --Does it contain id and objectives? if(questPacket[1 + offset]) then -- Create empty quest. local questPacketid = questPacket[offset] local objectiveCount = questPacket[offset+1] local objectives = {} offset = offset + 2 local objectiveIndex = 0 while objectiveIndex < objectiveCount and questPacket[offset] do objectiveIndex = objectiveIndex + 1 objectives[objectiveIndex] = {}; objectives[objectiveIndex].index = objectiveIndex; objectives[objectiveIndex].id = questPacket[offset] objectives[objectiveIndex].type = string.char(questPacket[offset+1])--[_QuestieComms.idLookup["type"]]; objectives[objectiveIndex].fulfilled = questPacket[offset+2]--[_QuestieComms.idLookup["fulfilled"]]; objectives[objectiveIndex].required = questPacket[offset+3]--[_QuestieComms.idLookup["required"]]; objectives[objectiveIndex].finished = objectives[objectiveIndex].fulfilled == objectives[objectiveIndex].required--[_QuestieComms.idLookup["finished"]]; allDone = allDone and objectives[objectiveIndex].finished offset = offset + 4 end if not (allDone and disableCompleteQuests) then if not QuestieComms.remoteQuestLogs[questPacketid] then QuestieComms.remoteQuestLogs[questPacketid] = {} end -- Create empty player. if not QuestieComms.remoteQuestLogs[questPacketid][playerName] then QuestieComms.remoteQuestLogs[questPacketid][playerName] = {} end QuestieComms.remoteQuestLogs[questPacketid][playerName] = objectives --Write to tooltip data QuestieComms.data:RegisterTooltip(questPacketid, playerName, objectives) end end end return offset, allDone end function QuestieComms:InsertQuestDataPacketV2(questPacket, playerName, offset, disableCompleteQuests) --We don't want to insert our own quest data. local allDone = true if questPacket then --Does it contain id and objectives? if(questPacket[2 + offset]) then -- Create empty quest. local questPacketid = questPacket[offset] local objectiveCount = questPacket[offset+1] local class = _indexToClass[questPacket[offset+2]] local objectives = {} offset = offset + 3 local objectiveIndex = 0 while objectiveIndex < objectiveCount and questPacket[offset] do objectiveIndex = objectiveIndex + 1 objectives[objectiveIndex] = {}; objectives[objectiveIndex].index = objectiveIndex; objectives[objectiveIndex].id = questPacket[offset] objectives[objectiveIndex].type = string.char(questPacket[offset+1])--[_QuestieComms.idLookup["type"]]; objectives[objectiveIndex].fulfilled = questPacket[offset+2]--[_QuestieComms.idLookup["fulfilled"]]; objectives[objectiveIndex].required = questPacket[offset+3]--[_QuestieComms.idLookup["required"]]; objectives[objectiveIndex].finished = objectives[objectiveIndex].fulfilled == objectives[objectiveIndex].required--[_QuestieComms.idLookup["finished"]]; allDone = allDone and objectives[objectiveIndex].finished offset = offset + 4 end if not (allDone and disableCompleteQuests) then if not QuestieComms.remoteQuestLogs[questPacketid] then QuestieComms.remoteQuestLogs[questPacketid] = {} end -- Create empty player. if not QuestieComms.remoteQuestLogs[questPacketid][playerName] then QuestieComms.remoteQuestLogs[questPacketid][playerName] = {} end QuestieComms.remoteQuestLogs[questPacketid][playerName] = objectives QuestieComms.remotePlayerClasses[playerName] = class --Write to tooltip data QuestieComms.data:RegisterTooltip(questPacketid, playerName, objectives) elseif disableCompleteQuests then -- remove player if they exist if QuestieComms.remoteQuestLogs[questPacketid] and QuestieComms.remoteQuestLogs[questPacketid][playerName] then QuestieComms.remoteQuestLogs[questPacketid][playerName] = nil end end end end return offset, allDone end function QuestieComms:CheckInGroup(name) return IsInRaid() and UnitInRaid(name) or UnitInParty(name) end function QuestieComms:RemoveAllRemotePlayers() for name in pairs(QuestieComms.remotePlayerTimes) do QuestieComms:RemoveRemotePlayer(name) end end function QuestieComms:RemoveRemotePlayer(name) QuestieComms.remotePlayerTimes[name] = nil QuestieComms.remotePlayerEnabled[name] = nil QuestieComms.remotePlayerClasses[name] = nil if not QuestieComms:CheckInGroup(name) then for _, players in pairs(QuestieComms.remoteQuestLogs) do players[name] = nil end end end function QuestieComms:SortRemotePlayers() local now = GetTime() local current = {} for name, time in pairs(QuestieComms.remotePlayerTimes) do if now - time > 60 * 4 then -- not seen in the last 4 minutes QuestieComms:RemoveRemotePlayer(name) else tinsert(current, name) end end table.sort(current, function(a, b) return QuestieComms.remotePlayerTimes[a] < QuestieComms.remotePlayerTimes[b] end) for index,name in pairs(current) do if index < 4 then QuestieComms.remotePlayerEnabled[name] = true else QuestieComms.remotePlayerEnabled[name] = false end end end QuestieComms._yellWaitingQuests = {} QuestieComms._yellQueue = {} QuestieComms._isYelling = false local _loadupTime_removeme = GetTime() -- this will be removed in 6.0.1 or 6.1, when we can figure out a proper way to prevent -- yelling quests on login. Not enough time to make and test a proper fix function QuestieComms:YellProgress(questId) if Questie.db.profile.disableYellComms or badYellLocations[C_Map.GetBestMapForUnit("player")] or QuestiePlayer.numberOfGroupMembers > 4 or GetTime() - _loadupTime_removeme < 8 then return end if not QuestieComms._yellWaitingQuests[questId] then QuestieComms._yellWaitingQuests[questId] = true if QuestieComms._isYelling then tinsert(QuestieComms._yellQueue, questId) else QuestieComms._isYelling = true C_Timer.After(2, function() _DoYell(questId) end) end end end _DoYell = function(questId) --[[local data = {} local _, count = QuestieComms:PopulateQuestDataPacketV2(questId, data, 1) if count > 0 then -- dont send quests with no objectives local packet = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_YELL_PROGRESS); packet.data[1] = data; packet.data.priority = "BULK" packet.data.writeMode = _QuestieComms.QC_WRITE_YELL packet:write(); QuestieComms._yellWaitingQuests[questId] = nil end local nextQuest = tremove(QuestieComms._yellQueue, 1) if nextQuest then C_Timer.After(2, function() _DoYell(nextQuest) end) else QuestieComms._isYelling = false end]] end _QuestieComms._isBroadcasting = false _QuestieComms._needsNewBroadcast = false _QuestieComms._nextBroadcastData = {} function _QuestieComms:BroadcastQuestLog(eventName, sendMode, targetPlayer) -- broadcast quest update to group or raid if _QuestieComms._isBroadcasting then tinsert(_QuestieComms._nextBroadcastData, {eventName, sendMode, targetPlayer}) return end local partyType = QuestiePlayer:GetGroupType() Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieComms] Message", eventName, "partyType:", tostring(partyType)) if partyType then local sorted = {} for questId, data in pairs(QuestLogCache.questLog_DO_NOT_MODIFY) do -- DO NOT MODIFY THE RETURNED TABLE if (not QuestieDB.QuestPointers[questId]) then if not Questie._sessionWarnings[questId] then if not Questie.IsSoD then Questie:Error(l10n("The quest %s is missing from Questie's database. Please report this on GitHub or Discord!", tostring(questId))) end Questie._sessionWarnings[questId] = true end else local questType = data.questTag local entry = { questId = questId, questType = questType, zoneOrSort = QuestieDB.QueryQuestSingle(questId, "zoneOrSort"), isSoloQuest = not (questType == "Dungeon" or questType == "Raid" or questType == "Group" or questType == "Elite" or questType == "PVP"), } if entry.zoneOrSort > 0 then entry.UiMapId = ZoneDB:GetUiMapIdByAreaId(entry.zoneOrSort) entry.zoneDistance = HBD:GetZoneDistance(entry.UiMapId, 0.5, 0.5, HBD:GetPlayerZone(), 0.5, 0.5) or 99999999 else entry.zoneDistance = 99999999 -- some high number (class quests etc) end if partyType ~= "raid" or (not entry.isSoloQuest) then tinsert(sorted, entry) end end end table.sort(sorted, function(a, b) if a.isSoloQuest and not b.isSoloQuest then return false elseif b.isSoloQuest and not a.isSoloQuest then return true else--if a.isSoloQuest == b.isSoloQuest then if a.zoneDistance > b.zoneDistance then return false elseif a.zoneDistance < b.zoneDistance then return true else return false -- 0 end end end) local rawQuestList = {} local blocks = {} local entryCount = 0 local blockCount = 2 -- the extra tick allows checking tremove() == nil to set _isBroadcasting=false for _, entry in pairs(sorted) do local quest = QuestieComms:CreateQuestDataPacket(entry.questId); --print("[CommsSendOrder][Block " .. (blockCount - 1) .. "] " .. QuestieDB.QueryQuestSingle(entry.questId, "name")) entryCount = entryCount + 1 rawQuestList[quest.id] = quest; if string.len(QuestieSerializer:Serialize(rawQuestList)) > 200 then--extra space for packet metadata and CTL stuff rawQuestList[quest.id] = nil tinsert(blocks, rawQuestList) rawQuestList = { [quest.id] = quest } entryCount = 1 blockCount = blockCount + 1 end end if entryCount ~= 0 then tinsert(blocks, rawQuestList) -- add the last block _QuestieComms._isBroadcasting = true -- hopefully reduce server load by staggering responses C_Timer.After(random() * 3, function() C_Timer.NewTicker(3, function() local block = tremove(blocks, 1) if block then -- send the block local questPacket = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_BROADCAST_FULL_QUESTLIST); questPacket.data.rawQuestList = block; if "WHISPER" == sendMode then questPacket.data.writeMode = _QuestieComms.QC_WRITE_WHISPER questPacket.data.target = targetPlayer questPacket.data.priority = "NORMAL" else if partyType == "raid" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLRAID questPacket.data.priority = "BULK" elseif partyType == "instance" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLINSTANCE questPacket.data.priority = "BULK" -- in case of battlegrounds else questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLGROUP questPacket.data.priority = "NORMAL" end end questPacket:write(); else _QuestieComms._isBroadcasting = false local nextBroadcast = tremove(_QuestieComms._nextBroadcastData, 1) if nextBroadcast then _QuestieComms:BroadcastQuestLog(unpack(nextBroadcast)) end end end, blockCount) end) end end end _QuestieComms._isBroadcastingV2 = false _QuestieComms._nextBroadcastDataV2 = {} function _QuestieComms:BroadcastQuestLogV2(eventName, sendMode, targetPlayer) -- broadcast quest update to group or raid if _QuestieComms._isBroadcastingV2 then tinsert(_QuestieComms._nextBroadcastDataV2, {eventName, sendMode, targetPlayer}) return end local partyType = QuestiePlayer:GetGroupType() Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieComms] Message", eventName, "partyType:", tostring(partyType)) if partyType then local sorted = {} for questId, data in pairs(QuestLogCache.questLog_DO_NOT_MODIFY) do -- DO NOT MODIFY THE RETURNED TABLE if (not QuestieDB.QuestPointers[questId]) then if not Questie._sessionWarnings[questId] then if not Questie.IsSoD then Questie:Error(l10n("The quest %s is missing from Questie's database. Please report this on GitHub or Discord!", tostring(questId))) end Questie._sessionWarnings[questId] = true end else local questType = data.questTag local entry = { questId = questId, questType = questType, zoneOrSort = QuestieDB.QueryQuestSingle(questId, "zoneOrSort"), isSoloQuest = not (questType == "Dungeon" or questType == "Raid" or questType == "Group" or questType == "Elite" or questType == "PVP"), } if entry.zoneOrSort > 0 then entry.UiMapId = ZoneDB:GetUiMapIdByAreaId(entry.zoneOrSort) entry.zoneDistance = HBD:GetZoneDistance(entry.UiMapId, 0.5, 0.5, HBD:GetPlayerZone(), 0.5, 0.5) or 99999999 else entry.zoneDistance = 99999999 -- some high number (class quests etc) end if partyType ~= "raid" or (not entry.isSoloQuest) then tinsert(sorted, entry) end end end table.sort(sorted, function(a, b) if a.isSoloQuest and not b.isSoloQuest then return false elseif b.isSoloQuest and not a.isSoloQuest then return true else--if a.isSoloQuest == b.isSoloQuest then if a.zoneDistance > b.zoneDistance then return false elseif a.zoneDistance < b.zoneDistance then return true else return false -- 0 end end end) local rawQuestList = {} local blocks = {} local entryCount = 0 local blockCount = 2 -- the extra tick allows checking tremove() == nil to set _isBroadcasting=false local offset = 2 for _, entry in pairs(sorted) do --print("[CommsSendOrder][Block " .. (blockCount - 1) .. "] " .. QuestieDB.QueryQuestSingle(entry.questId, "name")) entryCount = entryCount + 1 offset = QuestieComms:PopulateQuestDataPacketV2_noclass_renameme(entry.questId, rawQuestList, offset) if string.len(QuestieSerializer:Serialize(rawQuestList)) > 200 then--extra space for packet metadata and CTL stuff rawQuestList[1] = entryCount tinsert(blocks, rawQuestList) rawQuestList = {} entryCount = 0 blockCount = blockCount + 1 offset = 2 end end if entryCount ~= 0 or blockCount ~= 2 then rawQuestList[1] = entryCount tinsert(blocks, rawQuestList) -- add the last block _QuestieComms._isBroadcastingV2 = true -- hopefully reduce server load by staggering responses C_Timer.After(random() * 3, function() C_Timer.NewTicker(3, function() local block = tremove(blocks, 1) if block then -- send the block local questPacket = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_BROADCAST_FULL_QUESTLISTV2); questPacket.data[1] = block; if "WHISPER" == sendMode then questPacket.data.writeMode = _QuestieComms.QC_WRITE_WHISPER questPacket.data.target = targetPlayer questPacket.data.priority = "NORMAL" else if partyType == "raid" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLRAID questPacket.data.priority = "BULK" elseif partyType == "instance" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLINSTANCE questPacket.data.priority = "BULK" -- in case of battlegrounds else questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLGROUP questPacket.data.priority = "NORMAL" end end questPacket:write(); else _QuestieComms._isBroadcastingV2 = false local nextBroadcast = tremove(_QuestieComms._nextBroadcastDataV2, 1) if nextBroadcast then _QuestieComms:BroadcastQuestLogV2(unpack(nextBroadcast)) end end end, blockCount) end) end end end -- The "Hi" of questie, request others to send their questlog. function _QuestieComms:RequestQuestLog(eventName) -- broadcast quest update to group or raid local partyType = QuestiePlayer:GetGroupType() Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieComms] Message", eventName, "partyType:", tostring(partyType)) if partyType then --Do we really need to make this? local questPacket = _QuestieComms:CreatePacket(_QuestieComms.QC_ID_REQUEST_FULL_QUESTLIST); questPacket.data.priority = "NORMAL"; if partyType == "raid" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLRAID; elseif partyType == "instance" then questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLINSTANCE else questPacket.data.writeMode = _QuestieComms.QC_WRITE_ALLGROUP; end questPacket:write(); end end ---@param questId number ---@return QuestPacket function QuestieComms:CreateQuestDataPacket(questId) local questObject = QuestieDB.GetQuest(questId); ---@class QuestPacket local quest = { id = questId, objectives = {}, } local rawObjectives = QuestLogCache.GetQuestObjectives(questId) -- DO NOT MODIFY THE RETURNED TABLE if (not rawObjectives) then return quest end if questObject and next(questObject.Objectives) then for objectiveIndex, objective in pairs(rawObjectives) do -- DO NOT MODIFY THE RETURNED TABLE if questObject.Objectives[objectiveIndex] then quest.objectives[objectiveIndex] = { id = questObject.Objectives[objectiveIndex].Id, typ = string.sub(objective.type, 1, 1), fin = objective.finished, ful = objective.numFulfilled, req = objective.numRequired, } else Questie:Error(l10n("Missing objective data for quest "), tostring(questId), " ", tostring(objectiveIndex)) end end end Questie:Debug(Questie.DEBUG_SPAM, "[QuestieComms] questPacket made: Objectivetable:", quest.objectives) return quest; end ---@param questPacket QuestPacket @A packet created from the CreateQuestDataPacket function ---@param playerName string @The player said package should be added to. function QuestieComms:InsertQuestDataPacket(questPacket, playerName) --We don't want to insert our own quest data. if questPacket and playerName ~= UnitName("player") then --Does it contain id and objectives? if(questPacket.objectives and questPacket.id) then -- Create empty quest. if not QuestieComms.remoteQuestLogs[questPacket.id] then QuestieComms.remoteQuestLogs[questPacket.id] = {} end -- Create empty player. if not QuestieComms.remoteQuestLogs[questPacket.id][playerName] then QuestieComms.remoteQuestLogs[questPacket.id][playerName] = {} end local objectives = {} for objectiveIndex, objectiveData in pairs(questPacket.objectives) do --This is to check that all the data we require exist. objectives[objectiveIndex] = { index = objectiveIndex, id = objectiveData.id, type = objectiveData.typ, finished = objectiveData.fin, fulfilled = objectiveData.ful, required = objectiveData.req, } end QuestieComms.remoteQuestLogs[questPacket.id][playerName] = objectives; --Write to tooltip data QuestieComms.data:RegisterTooltip(questPacket.id, playerName, objectives); end end end _QuestieComms.packets = { [_QuestieComms.QC_ID_BROADCAST_QUEST_UPDATE] = { --1 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_BROADCAST_QUEST_UPDATE") _QuestieComms:Broadcast(self.data); end, read = function(remoteQuestPacket) if not remoteQuestPacket then Questie:Error("[QuestieComms] QC_ID_BROADCAST_QUEST_UPDATE no remoteQuestPacket") end --These are not strictly needed but helps readability. local playerName = remoteQuestPacket.playerName; local quest = remoteQuestPacket.quest; Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_BROADCAST_QUEST_UPDATE Player:", playerName) QuestieComms:InsertQuestDataPacket(quest, playerName); end }, [_QuestieComms.QC_ID_BROADCAST_QUEST_REMOVE] = { --2 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_BROADCAST_QUEST_REMOVE") _QuestieComms:Broadcast(self.data); end, read = function(remoteQuestPacket) if not remoteQuestPacket then Questie:Error("[QuestieComms] QC_ID_BROADCAST_QUEST_REMOVE no remoteQuestPacket") end Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieComms] Received: QC_ID_BROADCAST_QUEST_REMOVE") local playerName = remoteQuestPacket.playerName; local questId = remoteQuestPacket.id; if(QuestieComms.remoteQuestLogs[questId] and QuestieComms.remoteQuestLogs[questId][playerName]) then Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Removed quest:", questId, "for player:", playerName); QuestieComms.remoteQuestLogs[questId][playerName] = nil; end QuestieComms.data:RemoveQuestFromPlayer(questId, playerName); end }, [_QuestieComms.QC_ID_BROADCAST_FULL_QUESTLIST] = { --10 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_BROADCAST_FULL_QUESTLIST") _QuestieComms:Broadcast(self.data); end, read = function(remoteQuestList) if not remoteQuestList then Questie:Error("[QuestieComms] QC_ID_BROADCAST_FULL_QUESTLIST no remoteQuestList") end --These are not strictly needed but helps readability. local playerName = remoteQuestList.playerName; local questList = remoteQuestList.rawQuestList; Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_BROADCAST_FULL_QUESTLIST Player:", playerName, questList) --Don't save our own quests. if questList then for _, questData in pairs(questList) do QuestieComms:InsertQuestDataPacket(questData, playerName); end end end }, [_QuestieComms.QC_ID_REQUEST_FULL_QUESTLIST] = { --11 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_REQUEST_FULL_QUESTLIST") _QuestieComms:Broadcast(self.data); end, read = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_REQUEST_FULL_QUESTLIST") --Questie:SendMessage("QC_ID_BROADCAST_FULL_QUESTLIST"); --if tonumber(major) > 5 then -- QuestieComms:BroadcastQuestLogV2(self.playerName, "WHISPER")--Questie:SendMessage("QC_ID_BROADCAST_FULL_QUESTLISTV2"); --else -- QuestieComms:BroadcastQuestLogV2(self.playerName, "WHISPER") -- player doesnt have new questie, use old packet --end if UnitName("Player") ~= self.playerName then local major, _, _ = strsplit(".", self.ver) if tonumber(major) > 5 then _QuestieComms:BroadcastQuestLogV2("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", self.playerName) else _QuestieComms:BroadcastQuestLog("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", self.playerName) end end end }, [_QuestieComms.QC_ID_BROADCAST_FULL_QUESTLISTV2] = {-- 12 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_REQUEST_FULL_QUESTLISTV2") _QuestieComms:Broadcast(self.data); end, read = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_REQUEST_FULL_QUESTLISTV2") local offset = 2 local count = self[1][1] for _= 1, count do offset = QuestieComms:InsertQuestDataPacketV2_noclass_RenameMe(self[1], self.playerName, offset, false) end end }, [_QuestieComms.QC_ID_YELL_PROGRESS] = { --13 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_YELL_PROGRESS") if not badYellLocations[C_Map.GetBestMapForUnit("player")] then _QuestieComms:Broadcast(self.data); end end, read = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_YELL_PROGRESS") if not Questie.db.profile.disableYellComms and not badYellLocations[C_Map.GetBestMapForUnit("player")] then QuestieComms.remotePlayerTimes[self.playerName] = GetTime() QuestieComms:InsertQuestDataPacketV2(self[1], self.playerName, 1, true) QuestieComms:SortRemotePlayers() end end }, [_QuestieComms.QC_ID_BROADCAST_QUEST_UPDATEV2] = { -- 14 write = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Sending: QC_ID_BROADCAST_QUEST_UPDATEV2") _QuestieComms:Broadcast(self.data); end, read = function(self) Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms] Received: QC_ID_BROADCAST_QUEST_UPDATEV2") QuestieComms:InsertQuestDataPacketV2_noclass_RenameMe(self[1], self.playerName, 1, false) end } } -- Renamed Write function function _QuestieComms:Broadcast(packet) -- If the priority is not set, it must not be very important if packet.writeMode ~= _QuestieComms.QC_WRITE_WHISPER and (QuestiePlayer.numberOfGroupMembers > 15 or UnitInBattleground("Player")) then -- dont broadcast to large raids return end if(not packet.priority) then packet.priority = "BULK"; end local packetPriority = packet.priority local packetWriteMode = packet.writeMode local packetTarget = packet.target packet.priority = nil packet.target = nil packet.writeMode = nil -- we dont need to include these in the packet data if packetWriteMode == _QuestieComms.QC_WRITE_WHISPER then local compressedData = QuestieSerializer:Serialize(packet); Questie:Debug(Questie.DEBUG_DEVELOP,"send(|cFFFF2222", string.len(compressedData), "|r)") Questie:SendCommMessage(_QuestieComms.prefix, compressedData, packetWriteMode, packetTarget, packetPriority) elseif packetWriteMode == _QuestieComms.QC_WRITE_CHANNEL then local compressedData = QuestieSerializer:Serialize(packet); Questie:Debug(Questie.DEBUG_DEVELOP,"send(|cFFFF2222", string.len(compressedData), "|r)") -- Always do channel messages as BULK priority Questie:SendCommMessage(_QuestieComms.prefix, compressedData, packetWriteMode, GetChannelName("questiecom"), "BULK") --OLD: C_ChatInfo.SendAddonMessage("questie", compressedData, "CHANNEL", GetChannelName("questiecom")) elseif packetWriteMode == _QuestieComms.QC_WRITE_YELL then packet.msgVer = nil --packet.ver = nil local compressedData = QuestieSerializer:Serialize(packet, "b89"); --print("Yelling progress: " .. compressedData) Questie:SendCommMessage(_QuestieComms.prefix, compressedData, packetWriteMode, "BULK") else local compressedData = QuestieSerializer:Serialize(packet); Questie:Debug(Questie.DEBUG_DEVELOP, "send(|cFFFF2222", string.len(compressedData), "|r)") Questie:SendCommMessage(_QuestieComms.prefix, compressedData, packetWriteMode, nil, packetPriority) --OLD: C_ChatInfo.SendAddonMessage("questie", compressedData, packet.writeMode) end end function _QuestieComms:OnCommReceived(message, distribution, sender) pcall(_QuestieComms.OnCommReceived_unsafe, _QuestieComms, message, distribution, sender) end function _QuestieComms:OnCommReceived_unsafe(message, distribution, sender) --print("[" .. distribution .."][" .. sender .. "] " .. message) Questie:Debug(Questie.DEBUG_DEVELOP, "|cFF22FF22", "sender:", "|r", sender, "distribution:", distribution, "Packet length:",string.len(message)) if message and sender and sender ~= UnitName("player") then local decompressedData if distribution == "YELL" then --print("Decompressing YELL data") decompressedData = QuestieSerializer:Deserialize(message, "b89") else --print("Decompressing normal data") decompressedData = QuestieSerializer:Deserialize(message) end --Check if the message version is the same base value if distribution == "YELL" and decompressedData.msgId and _QuestieComms.packets[decompressedData.msgId] then decompressedData.playerName = sender; Questie:Debug(Questie.DEBUG_DEVELOP, "Executing message ID: ", decompressedData.msgId, "From: ", sender) _QuestieComms.packets[decompressedData.msgId].read(decompressedData) elseif(decompressedData and decompressedData.msgVer and floor(decompressedData.msgVer) == floor(commMessageVersion)) then if(decompressedData and decompressedData.msgId and _QuestieComms.packets[decompressedData.msgId]) then --If a new version exist, tell them! if(suggestUpdate) then local major, minor, patch = strsplit(".", decompressedData.ver); local majorOwn, minorOwn, patchOwn = QuestieLib:GetAddonVersionInfo(); if(majorOwn < tonumber(major) or (majorOwn == tonumber(major) and minorOwn < tonumber(minor)) or (majorOwn == tonumber(major) and minorOwn == tonumber(minor) and patchOwn < tonumber(patch)) and (not UnitAffectingCombat("player"))) then suggestUpdate = false; if(majorOwn < tonumber(major)) then Questie:Print("|cffff0000", l10n("A Major patch for Questie exists!"), "|r"); Questie:Print("|cffff0000", l10n("Please update as soon as possible!"), "|r"); else Questie:Print("|cffff0000", l10n("You have an outdated version of Questie!"), "|r"); Questie:Print("|cffff0000", l10n("Please consider updating!"), "|r"); end end end decompressedData.playerName = sender; Questie:Debug(Questie.DEBUG_DEVELOP, "Executing message ID: ", decompressedData.msgId, "From: ", sender, "MessageVersion:", decompressedData.msgVer) _QuestieComms.packets[decompressedData.msgId].read(decompressedData); else Questie:Debug(Questie.DEBUG_INFO, "[QuestieComms]", decompressedData, decompressedData.msgId, _QuestieComms.packets[decompressedData.msgId]) Questie:Error("Error reading QuestieComm message (If it persist try updating) Player:", sender, "PacketLength:", string.len(message)); end elseif(decompressedData and not warnedUpdate and decompressedData.msgVer) then -- We want to know who actually is the one with the mismatched version! if(floor(commMessageVersion) < floor(decompressedData.msgVer)) then Questie:Error(l10n("You have an incompatible QuestieComms message! Please update!"), l10n(" Yours: v"), commMessageVersion, sender..": v", decompressedData.msgVer); elseif(floor(commMessageVersion) > floor(decompressedData.msgVer)) then Questie:Print("|cFFFF0000", l10n("WARNING!"), "|r", sender, l10n("has an incompatible Questie version, QuestieComms won't work!"), l10n(" Yours: v"), commMessageVersion, sender..": v", decompressedData.msgVer) end warnedUpdate = true; end end end -- Copied: Is this really needed? Can't we just optimize away this? function _QuestieComms:CreatePacket(messageId) -- Duplicate the object. local pkt = {}; for k,v in pairs(_QuestieComms.packets[messageId]) do pkt[k] = v end local major, minor, patch = QuestieLib:GetAddonVersionInfo(); -- Set messageId pkt.data = { ver = major.."."..minor.."."..patch, msgVer = commMessageVersion, msgId = messageId, } -- Some messages initialize if pkt.init then pkt:init() end return pkt end function QuestieComms:ResetAll() QuestieComms.data:ResetAll() QuestieComms.remoteQuestLogs = {} end return QuestieComms