Files
Questie-X/Modules/Network/QuestieComms.lua
T
2026-06-04 17:04:12 -05:00

1191 lines
51 KiB
Lua

---@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
if not IsQuestieCommsEnabled() then return end
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
if not IsQuestieCommsEnabled() then return end
local partyType = QuestiePlayer:GetGroupType()
Questie:Debug(Questie.DEBUG_COMMS, "[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
local DEFAULT_QUEST_LIST_PACKET_SIZE_LIMIT = 200
local DEFAULT_QUEST_LIST_INITIAL_JITTER = 3
local DEFAULT_QUEST_LIST_BLOCK_INTERVAL = 3
local function GetProfileNumber(key, defaultValue, minValue, maxValue)
if not Questie or not Questie.db or not Questie.db.profile then
return defaultValue
end
local value = Questie.db.profile[key]
if type(value) ~= "number" then
return defaultValue
end
if value < minValue then
return minValue
end
if value > maxValue then
return maxValue
end
return value
end
local function GetQuestListPacketSizeLimit()
return GetProfileNumber("questieCommsQuestListPacketSize", DEFAULT_QUEST_LIST_PACKET_SIZE_LIMIT, 100, 500)
end
local function GetQuestListInitialJitter()
return GetProfileNumber("questieCommsQuestListInitialJitter", DEFAULT_QUEST_LIST_INITIAL_JITTER, 0, 10)
end
local function GetQuestListBlockInterval()
return GetProfileNumber("questieCommsQuestListBlockInterval", DEFAULT_QUEST_LIST_BLOCK_INTERVAL, 0.5, 10)
end
local function IsQuestieCommsEnabled()
if not Questie or not Questie.db or not Questie.db.profile then
return true
end
return Questie.db.profile.questieCommsEnabled ~= false
end
local function GetSerializedPacketSize(packet)
return string.len(QuestieSerializer:Serialize(packet))
end
local function QueuePush(queue, queueState, value)
queueState.tail = queueState.tail + 1
queue[queueState.tail] = value
end
local function QueuePop(queue, queueState)
if queueState.head > queueState.tail then
queueState.head = 1
queueState.tail = 0
return nil
end
local value = queue[queueState.head]
queue[queueState.head] = nil
queueState.head = queueState.head + 1
if queueState.head > queueState.tail then
queueState.head = 1
queueState.tail = 0
end
return value
end
local function GetQuestDataPacketV2Size(questId)
local questObject = QuestieDB.GetQuest(questId)
local rawObjectives = QuestLogCache.GetQuestObjectives(questId) -- DO NOT MODIFY THE RETURNED TABLE
if (not rawObjectives) or (not questObject) or (not questObject.Objectives) or (not next(questObject.Objectives)) then
return 0
end
local questPacket = {}
local offset = 1
local count = 0
questPacket[offset] = questId
local countOffset = offset + 1
offset = offset + 2
for objectiveIndex, objective in pairs(rawObjectives) do -- DO NOT MODIFY THE RETURNED TABLE
local objectiveData = questObject.Objectives[objectiveIndex]
if objectiveData then
questPacket[offset] = objectiveData.Id
questPacket[offset + 1] = string.byte(string.sub(objective.type, 1, 1))
questPacket[offset + 2] = objective.numFulfilled
questPacket[offset + 3] = objective.numRequired
offset = offset + 4
count = count + 1
end
end
questPacket[countOffset] = count
return GetSerializedPacketSize(questPacket)
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
function QuestieComms:YellProgress(questId)
if not IsQuestieCommsEnabled() then return end
if Questie.db.profile.disableYellComms or badYellLocations[C_Map.GetBestMapForUnit("player")] or QuestiePlayer.numberOfGroupMembers > 4 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
-- Yell progress feature is currently disabled
end
end
end
_QuestieComms._isBroadcasting = false
_QuestieComms._needsNewBroadcast = false
_QuestieComms._nextBroadcastData = {}
_QuestieComms._nextBroadcastDataState = { head = 1, tail = 0 }
function _QuestieComms:BroadcastQuestLog(eventName, sendMode, targetPlayer) -- broadcast quest update to group or raid
if not IsQuestieCommsEnabled() then return end
if _QuestieComms._isBroadcasting then
QueuePush(_QuestieComms._nextBroadcastData, _QuestieComms._nextBroadcastDataState, {eventName, sendMode, targetPlayer})
return
end
local partyType = QuestiePlayer:GetGroupType()
Questie:Debug(Questie.DEBUG_COMMS, "[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 questListPacketSizeLimit = GetQuestListPacketSizeLimit()
local rawQuestList = {}
local blocks = {}
local blockState = { head = 1, tail = 0 }
local entryCount = 0
local blockCount = 2 -- the extra tick allows checking tremove() == nil to set _isBroadcasting=false
local blockSerializedSize = 2
for _, entry in pairs(sorted) do
local quest = QuestieComms:CreateQuestDataPacket(entry.questId);
--print("[CommsSendOrder][Block " .. (blockCount - 1) .. "] " .. QuestieDB.QueryQuestSingle(entry.questId, "name"))
local questPacketSize = GetSerializedPacketSize(quest)
if entryCount ~= 0 and (blockSerializedSize + questPacketSize) > questListPacketSizeLimit then
QueuePush(blocks, blockState, rawQuestList)
rawQuestList = {}
entryCount = 0
blockSerializedSize = 2
blockCount = blockCount + 1
end
entryCount = entryCount + 1
rawQuestList[quest.id] = quest;
blockSerializedSize = blockSerializedSize + questPacketSize
end
if entryCount ~= 0 then
QueuePush(blocks, blockState, rawQuestList) -- add the last block
_QuestieComms._isBroadcasting = true
-- hopefully reduce server load by staggering responses
C_Timer.After(random() * GetQuestListInitialJitter(), function()
C_Timer.NewTicker(GetQuestListBlockInterval(), function()
local block = QueuePop(blocks, blockState)
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 = QueuePop(_QuestieComms._nextBroadcastData, _QuestieComms._nextBroadcastDataState)
if nextBroadcast then
_QuestieComms:BroadcastQuestLog(unpack(nextBroadcast))
end
end
end, blockCount)
end)
end
end
end
_QuestieComms._isBroadcastingV2 = false
_QuestieComms._nextBroadcastDataV2 = {}
_QuestieComms._nextBroadcastDataV2State = { head = 1, tail = 0 }
function _QuestieComms:BroadcastQuestLogV2(eventName, sendMode, targetPlayer) -- broadcast quest update to group or raid
if not IsQuestieCommsEnabled() then return end
if _QuestieComms._isBroadcastingV2 then
QueuePush(_QuestieComms._nextBroadcastDataV2, _QuestieComms._nextBroadcastDataV2State, {eventName, sendMode, targetPlayer})
return
end
local partyType = QuestiePlayer:GetGroupType()
Questie:Debug(Questie.DEBUG_COMMS, "[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 questListPacketSizeLimit = GetQuestListPacketSizeLimit()
local rawQuestList = {}
local blocks = {}
local blockState = { head = 1, tail = 0 }
local entryCount = 0
local blockCount = 2 -- the extra tick allows checking tremove() == nil to set _isBroadcasting=false
local blockSerializedSize = 2
local offset = 2
for _, entry in pairs(sorted) do
--print("[CommsSendOrder][Block " .. (blockCount - 1) .. "] " .. QuestieDB.QueryQuestSingle(entry.questId, "name"))
local questPacketSize = GetQuestDataPacketV2Size(entry.questId)
if entryCount ~= 0 and (blockSerializedSize + questPacketSize) > questListPacketSizeLimit then
rawQuestList[1] = entryCount
QueuePush(blocks, blockState, rawQuestList)
rawQuestList = {}
entryCount = 0
blockSerializedSize = 2
blockCount = blockCount + 1
offset = 2
end
entryCount = entryCount + 1
offset = QuestieComms:PopulateQuestDataPacketV2_noclass_renameme(entry.questId, rawQuestList, offset)
blockSerializedSize = blockSerializedSize + questPacketSize
end
if entryCount ~= 0 or blockCount ~= 2 then
rawQuestList[1] = entryCount
QueuePush(blocks, blockState, rawQuestList) -- add the last block
_QuestieComms._isBroadcastingV2 = true
-- hopefully reduce server load by staggering responses
C_Timer.After(random() * GetQuestListInitialJitter(), function()
C_Timer.NewTicker(GetQuestListBlockInterval(), function()
local block = QueuePop(blocks, blockState)
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 = QueuePop(_QuestieComms._nextBroadcastDataV2, _QuestieComms._nextBroadcastDataV2State)
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
if not IsQuestieCommsEnabled() then return end
local partyType = QuestiePlayer:GetGroupType()
Questie:Debug(Questie.DEBUG_COMMS, "[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_COMMS, "[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 not IsQuestieCommsEnabled() then return end
-- 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_COMMS,"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_COMMS,"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_COMMS, "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)
if not IsQuestieCommsEnabled() then return end
pcall(_QuestieComms.OnCommReceived_unsafe, _QuestieComms, message, distribution, sender)
end
function _QuestieComms:OnCommReceived_unsafe(message, distribution, sender)
--print("[" .. distribution .."][" .. sender .. "] " .. message)
Questie:Debug(Questie.DEBUG_COMMS, "|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_COMMS, "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_COMMS, "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