Files
pfQuest-turtle/pfQuest-partyprogress.lua

1261 lines
53 KiB
Lua

local PARTYPROGRESS_DEBUG = false
local function DebugPrint(msg)
if PARTYPROGRESS_DEBUG then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffcc[PartyProgress]|r " .. msg)
end
end
local partyQuestData = {}
local myQuestMappings = {}
local lastBroadcastState = {}
local previousQuestList = {}
local lastPartyPinSignature = nil
local function CleanupPartyData()
local validPlayers = {}
validPlayers[UnitName("player")] = true
for i = 1, GetNumPartyMembers() do
local name = UnitName("party" .. i)
if name then
validPlayers[name] = true
end
end
for playerName in pairs(partyQuestData) do
if not validPlayers[playerName] then
partyQuestData[playerName] = nil
end
end
end
local function RebuildQuestMappings()
if not pfDB or not pfDB["quests"] or not pfDB["quests"]["data"] then
return
end
if not pfDB["quests"]["enUS"] then
return
end
local activeQuests = {}
local activeQuestIds = {}
local unresolvedTitles = {}
local questIdByLogIndex = {}
if pfQuest and pfQuest.questlog then
for questId, state in pairs(pfQuest.questlog) do
if type(questId) == "number" and state and state.qlogid then
questIdByLogIndex[state.qlogid] = questId
end
end
end
for qid = 1, GetNumQuestLogEntries() do
local questTitle, _, _, _, _, complete = pfQuestCompat.GetQuestLogTitle(qid)
if questTitle and complete ~= 1 then
activeQuests[questTitle] = {}
local questId = questIdByLogIndex[qid]
if not questId and pfDatabase and pfDatabase.GetQuestIDs then
local preserveSelection = QuestLogFrame and QuestLogFrame:IsShown()
local ids = pfDatabase:GetQuestIDs(qid, preserveSelection)
questId = ids and tonumber(ids[1])
end
if questId then
activeQuestIds[questId] = true
else
unresolvedTitles[questTitle] = true
end
-- The client can briefly return nil while the quest log is
-- unavailable during transitions such as taking a flight path.
local numObjectives = tonumber(GetNumQuestLeaderBoards(qid)) or 0
for i = 1, numObjectives do
local text, objType, finished = GetQuestLogLeaderBoard(i, qid)
if text then
local _, _, objName, current, total = string.find(text, "(.*):%s*(%d+)%s*/%s*(%d+)")
if objName then
objName = string.gsub(objName, "^%s*(.-)%s*$", "%1")
table.insert(activeQuests[questTitle], {
objective = objName,
current = tonumber(current),
total = tonumber(total)
})
end
end
end
end
end
-- Party joins and quest turn-ins can produce several QUEST_LOG_UPDATE
-- events. Only inspect the handful of active quest records instead of
-- walking the entire quest database for every refresh. The title fallback
-- is retained for the brief client states where a log index has no ID yet.
local candidateQuests = {}
for questId in pairs(activeQuestIds) do
local localizedData = pfDB["quests"]["enUS"][questId]
if localizedData then candidateQuests[questId] = localizedData end
end
if next(unresolvedTitles) then
for questId, localizedData in pairs(pfDB["quests"]["enUS"]) do
if localizedData["T"] and unresolvedTitles[localizedData["T"]] then
candidateQuests[questId] = localizedData
end
end
end
for questId, localizedData in pairs(candidateQuests) do
local questTitle = localizedData["T"]
if questTitle and activeQuests[questTitle] then
local questData = pfDB["quests"]["data"][questId]
if questData and questData["obj"] then
if questData["obj"]["U"] then
for _, unitId in pairs(questData["obj"]["U"]) do
if pfDB["units"] and pfDB["units"]["enUS"] and pfDB["units"]["enUS"][unitId] then
local targetName = pfDB["units"]["enUS"][unitId]
for _, activeObj in ipairs(activeQuests[questTitle]) do
local objNameBase = string.gsub(activeObj.objective, " slain$", "")
objNameBase = string.gsub(objNameBase, " killed$", "")
if objNameBase == targetName or string.find(activeObj.objective, targetName, 1, true) then
if not myQuestMappings[targetName] then
myQuestMappings[targetName] = {}
end
local found = false
for _, data in ipairs(myQuestMappings[targetName]) do
if data.quest == questTitle and data.objective == activeObj.objective then
data.current = activeObj.current
data.total = activeObj.total
data.questId = questId
data.targetId = unitId
data.targetType = "U"
found = true
break
end
end
if not found then
table.insert(myQuestMappings[targetName], {
quest = questTitle,
questId = questId,
objective = activeObj.objective,
current = activeObj.current,
total = activeObj.total,
targetId = unitId,
targetType = "U"
})
end
end
end
end
end
end
if questData["obj"]["I"] then
for _, itemId in pairs(questData["obj"]["I"]) do
local itemName = nil
if pfDB["items"] and pfDB["items"]["enUS"] and pfDB["items"]["enUS"][itemId] then
itemName = pfDB["items"]["enUS"][itemId]
end
if pfDB["items"] and pfDB["items"]["data"] and pfDB["items"]["data"][itemId] then
local itemData = pfDB["items"]["data"][itemId]
if itemData["U"] then
for unitId, dropRate in pairs(itemData["U"]) do
if pfDB["units"] and pfDB["units"]["enUS"] and pfDB["units"]["enUS"][unitId] then
local npcName = pfDB["units"]["enUS"][unitId]
for _, activeObj in ipairs(activeQuests[questTitle]) do
if itemName and string.find(activeObj.objective, itemName, 1, true) then
if not myQuestMappings[npcName] then
myQuestMappings[npcName] = {}
end
local found = false
for _, data in ipairs(myQuestMappings[npcName]) do
if data.quest == questTitle and data.objective == activeObj.objective then
data.current = activeObj.current
data.total = activeObj.total
data.questId = questId
data.targetId = unitId
data.itemId = itemId
data.targetType = "I"
found = true
break
end
end
if not found then
table.insert(myQuestMappings[npcName], {
quest = questTitle,
questId = questId,
objective = activeObj.objective,
current = activeObj.current,
total = activeObj.total,
targetId = unitId,
itemId = itemId,
targetType = "I"
})
end
end
end
end
end
end
if itemData["O"] then
for objectId, dropRate in pairs(itemData["O"]) do
if pfDB["objects"] and pfDB["objects"]["enUS"] and pfDB["objects"]["enUS"][objectId] then
local objName = pfDB["objects"]["enUS"][objectId]
for _, activeObj in ipairs(activeQuests[questTitle]) do
if itemName and string.find(activeObj.objective, itemName, 1, true) then
if not myQuestMappings[objName] then
myQuestMappings[objName] = {}
end
local found = false
for _, data in ipairs(myQuestMappings[objName]) do
if data.quest == questTitle and data.objective == activeObj.objective then
data.current = activeObj.current
data.total = activeObj.total
data.questId = questId
data.targetId = objectId
data.itemId = itemId
data.targetType = "O"
found = true
break
end
end
if not found then
table.insert(myQuestMappings[objName], {
quest = questTitle,
questId = questId,
objective = activeObj.objective,
current = activeObj.current,
total = activeObj.total,
targetId = objectId,
itemId = itemId,
targetType = "O"
})
end
end
end
end
end
end
end
end
end
end
end
end
end
local function CaptureMyQuestData(targetKey, questTitle, objectiveText, current, total)
if not myQuestMappings[targetKey] then
myQuestMappings[targetKey] = {}
end
local found = false
for _, data in ipairs(myQuestMappings[targetKey]) do
if data.quest == questTitle and data.objective == objectiveText then
data.current = current
data.total = total
found = true
break
end
end
if not found then
table.insert(myQuestMappings[targetKey], {
quest = questTitle,
objective = objectiveText,
current = current,
total = total
})
end
end
local function BuildStateString(targetKey, questData)
return string.format("%s:%s:%d:%d", targetKey, questData.quest, questData.current, questData.total)
end
local function ShareQuestData(forceFullSync)
if GetNumPartyMembers() == 0 then
return
end
local channel = "PARTY"
local activeQuests = {}
for qid = 1, GetNumQuestLogEntries() do
local questTitle = pfQuestCompat.GetQuestLogTitle(qid)
if questTitle then
activeQuests[questTitle] = true
end
end
for questTitle in pairs(previousQuestList) do
if not activeQuests[questTitle] then
local questId = nil
for targetKey, quests in pairs(myQuestMappings) do
for _, questData in ipairs(quests) do
if questData.quest == questTitle and questData.questId then
questId = questData.questId
break
end
end
if questId then break end
end
if questId then
local msg = string.format("REMOVEQ:%d:%s", questId, questTitle)
SendAddonMessage("pfqt", msg, channel)
DebugPrint("REMOVE broadcast - Quest: " .. questTitle .. " (ID: " .. questId .. ")")
end
end
end
previousQuestList = activeQuests
for targetKey, quests in pairs(myQuestMappings) do
local i = 1
while i <= table.getn(quests) do
if not activeQuests[quests[i].quest] then
table.remove(quests, i)
else
i = i + 1
end
end
if table.getn(quests) == 0 then
myQuestMappings[targetKey] = nil
end
end
-- Read the quest log once, then apply progress to every mapped spawn.
-- Item objectives can map to many units/objects, so rescanning the full
-- log for each mapping caused a noticeable hitch during full party syncs.
local objectiveProgress = {}
for qid = 1, GetNumQuestLogEntries() do
local questTitle = pfQuestCompat.GetQuestLogTitle(qid)
if questTitle then
objectiveProgress[questTitle] = objectiveProgress[questTitle] or {}
local numObjectives = tonumber(GetNumQuestLeaderBoards(qid)) or 0
for i = 1, numObjectives do
local text = GetQuestLogLeaderBoard(i, qid)
if text then
local _, _, objName, current, total = string.find(text, "(.*):%s*(%d+)%s*/%s*(%d+)")
if objName then
objName = string.gsub(objName, "^%s*(.-)%s*$", "%1")
objectiveProgress[questTitle][objName] = {
current = tonumber(current), total = tonumber(total)
}
end
end
end
end
end
for _, quests in pairs(myQuestMappings) do
for _, questData in ipairs(quests) do
local progress = objectiveProgress[questData.quest]
and objectiveProgress[questData.quest][questData.objective]
if progress then
questData.current = progress.current
questData.total = progress.total
end
end
end
local changedEntries = {}
local currentState = {}
local consolidatedObjectives = {}
for targetKey, quests in pairs(myQuestMappings) do
for _, data in ipairs(quests) do
local stateKey = BuildStateString(targetKey, data)
currentState[stateKey] = true
if forceFullSync or not lastBroadcastState[stateKey] then
if data.questId and data.targetId and data.targetType then
if data.targetType == "I" then
local objKey = data.questId .. ":" .. data.objective
if not consolidatedObjectives[objKey] then
consolidatedObjectives[objKey] = {
questId = data.questId,
objective = data.objective,
quest = data.quest,
current = data.current,
total = data.total,
targetType = "I",
targetId = data.itemId or data.targetId
}
else
if data.current > consolidatedObjectives[objKey].current then
consolidatedObjectives[objKey].current = data.current
end
end
else
table.insert(changedEntries, {
targetKey = targetKey,
targetType = data.targetType,
targetId = data.targetId,
questId = data.questId,
current = data.current,
total = data.total,
quest = data.quest,
objective = data.objective
})
end
end
end
end
end
for _, entry in pairs(consolidatedObjectives) do
table.insert(changedEntries, entry)
end
lastBroadcastState = currentState
if table.getn(changedEntries) == 0 then
DebugPrint("no changes to broadcast")
return
end
local messages = {}
local currentBatch = "V1"
for _, entry in ipairs(changedEntries) do
local safeObjective = string.gsub(entry.objective or "", "[:;]", "")
local entryStr = string.format("%s%dQ%d:%d:%d:%s;", entry.targetType, entry.targetId, entry.questId, entry.current, entry.total, safeObjective)
if string.len(currentBatch) + string.len(entryStr) > 250 then
table.insert(messages, currentBatch)
currentBatch = "V1" .. entryStr
else
currentBatch = currentBatch .. entryStr
end
end
if string.len(currentBatch) > 2 then
table.insert(messages, currentBatch)
end
for i, msg in ipairs(messages) do
SendAddonMessage("pfqt", msg, channel)
end
end
local function ProcessQuestData(sender, message)
-- Some Turtle clients also fire CHAT_MSG_ADDON for messages we send to the
-- party. Local progress is already rendered by the normal quest tooltip;
-- retaining it here produces a duplicate line labelled with our own name.
if sender == UnitName("player") then
return
end
local _, _, removeQuestId, removeQuestTitle = string.find(message, "^REMOVEQ:(%d+):(.+)$")
if removeQuestId and removeQuestTitle then
if partyQuestData[sender] then
for targetKey, quests in pairs(partyQuestData[sender]) do
local i = 1
while i <= table.getn(quests) do
if quests[i].quest == removeQuestTitle then
table.remove(quests, i)
else
i = i + 1
end
end
if table.getn(quests) == 0 then
partyQuestData[sender][targetKey] = nil
end
end
end
return
end
if string.sub(message, 1, 2) == "V1" then
local batch = string.sub(message, 3)
local entries = {}
for entry in string.gfind(batch, "([^;]+)") do
table.insert(entries, entry)
end
for _, entry in ipairs(entries) do
local _, _, targetType, targetId, questId, current, total, objectiveText =
string.find(entry, "^([UIO])(%d+)Q(%d+):(%d+):(%d+):(.*)$")
if targetType and targetId and questId and current and total then
targetId = tonumber(targetId)
questId = tonumber(questId)
current = tonumber(current)
total = tonumber(total)
if not objectiveText or objectiveText == "" then
objectiveText = "Quest Objective"
end
local questTitle = nil
if pfDB and pfDB["quests"] and pfDB["quests"]["enUS"] and pfDB["quests"]["enUS"][questId] then
questTitle = pfDB["quests"]["enUS"][questId]["T"]
end
if not questTitle then
elseif targetType == "I" then
if pfDB and pfDB["items"] and pfDB["items"]["data"] and pfDB["items"]["data"][targetId] then
local itemData = pfDB["items"]["data"][targetId]
if itemData["U"] then
for unitId in pairs(itemData["U"]) do
if pfDB["units"] and pfDB["units"]["enUS"] and pfDB["units"]["enUS"][unitId] then
local npcName = pfDB["units"]["enUS"][unitId]
partyQuestData[sender] = partyQuestData[sender] or {}
partyQuestData[sender][npcName] = partyQuestData[sender][npcName] or {}
local found = false
for _, data in ipairs(partyQuestData[sender][npcName]) do
if data.quest == questTitle then
data.objective = objectiveText
data.current = current
data.total = total
data.questId = questId
data.targetId = unitId
data.targetType = "U"
data.itemId = targetId
found = true
break
end
end
if not found then
table.insert(partyQuestData[sender][npcName], {
quest = questTitle,
objective = objectiveText,
current = current,
total = total,
questId = questId,
targetId = unitId,
targetType = "U",
itemId = targetId
})
end
end
end
end
if itemData["O"] then
for objectId in pairs(itemData["O"]) do
if pfDB["objects"] and pfDB["objects"]["enUS"] and pfDB["objects"]["enUS"][objectId] then
local objName = pfDB["objects"]["enUS"][objectId]
partyQuestData[sender] = partyQuestData[sender] or {}
partyQuestData[sender][objName] = partyQuestData[sender][objName] or {}
local found = false
for _, data in ipairs(partyQuestData[sender][objName]) do
if data.quest == questTitle then
data.objective = objectiveText
data.current = current
data.total = total
data.questId = questId
data.targetId = objectId
data.targetType = "O"
data.itemId = targetId
found = true
break
end
end
if not found then
table.insert(partyQuestData[sender][objName], {
quest = questTitle,
objective = objectiveText,
current = current,
total = total,
questId = questId,
targetId = objectId,
targetType = "O",
itemId = targetId
})
end
end
end
end
end
else
local targetName = nil
if targetType == "U" and pfDB["units"] and pfDB["units"]["enUS"] then
targetName = pfDB["units"]["enUS"][targetId]
elseif targetType == "O" and pfDB["objects"] and pfDB["objects"]["enUS"] then
targetName = pfDB["objects"]["enUS"][targetId]
end
if targetName then
partyQuestData[sender] = partyQuestData[sender] or {}
partyQuestData[sender][targetName] = partyQuestData[sender][targetName] or {}
local found = false
for _, data in ipairs(partyQuestData[sender][targetName]) do
if data.quest == questTitle then
data.objective = objectiveText
data.current = current
data.total = total
data.questId = questId
data.targetId = targetId
data.targetType = targetType
found = true
break
end
end
if not found then
table.insert(partyQuestData[sender][targetName], {
quest = questTitle,
objective = objectiveText,
current = current,
total = total,
questId = questId,
targetId = targetId,
targetType = targetType
})
end
end
end
end
end
end
end
-- True when the local player also has this exact quest+objective active
-- (tracked via the same myQuestMappings this file already builds for its own
-- tooltip/broadcast use). When true, the player's own normal PFQUEST pin
-- already sits at this spot -- HookPfQuestTooltip already appends every
-- party member's progress to that pin's tooltip -- so a separate PFPARTY pin
-- would only be a redundant, differently-styled duplicate at the same coords.
local function LocalPlayerHasObjective(targetKey, questTitle, objectiveText)
local quests = myQuestMappings[targetKey]
if not quests then return false end
for _, localData in ipairs(quests) do
if localData.quest == questTitle and localData.objective == objectiveText then
return true
end
end
return false
end
-- Renders party members' active, incomplete quest objectives as map pins
-- under a dedicated "PFPARTY" addon namespace, kept separate from the
-- player's own "PFQUEST" pins so it never appears in the quest tracker
-- (tracker.lua only reads PFQUEST nodes). Only rendered for objectives the
-- local player does NOT also have active (see LocalPlayerHasObjective above)
-- -- otherwise the normal PFQUEST pin already covers it. Opt-in via the
-- showPartyQuestPins config checkbox. No meta.texture is set: map.lua's
-- UpdateNode gives an untextured PFPARTY node the same star shape as the
-- "fav" marker but tinted through the ordinary color-hash/click-to-recolor
-- path (see the frame.addon == "PFPARTY" branch there), and (since QTYPE
-- is not set here, layer defaults to 1 like any other raw spawn) becomes
-- eligible for the same raw-objective route-arrow targeting via route.lua's
-- SetRawObjectiveCandidates -- a dedicated meta.texture would have skipped
-- both (see the route-eligibility gates in map.lua for the opt-out).
local function RenderPartyQuestPins()
if not pfMap or not pfMap.DeleteNode then
return
end
local enabled = pfQuest_config and pfQuest_config["showPartyQuestPins"] == "1"
-- Cheap signature of what would actually be drawn. Several triggers call
-- this function far more often than the rendered pin set ever changes
-- (e.g. QUEST_LOG_UPDATE firing on unrelated log activity); tearing down
-- and rebuilding identical nodes on every one of those made a pin's
-- tooltip flicker/disappear if the player happened to be hovering it.
local sigParts = {}
if enabled then
for playerName, targets in pairs(partyQuestData) do
for targetKey, quests in pairs(targets) do
for _, data in ipairs(quests) do
if data.targetType and data.targetId and data.questId
and (data.current or 0) < (data.total or 0)
and not LocalPlayerHasObjective(targetKey, data.quest, data.objective) then
table.insert(sigParts, playerName .. ":" .. targetKey .. ":" .. data.quest
.. ":" .. data.current .. "/" .. data.total)
end
end
end
end
table.sort(sigParts)
end
local signature = enabled and table.concat(sigParts, ";") or "off"
if signature == lastPartyPinSignature then
return
end
lastPartyPinSignature = signature
pfMap:DeleteNode("PFPARTY")
if not enabled then
return
end
if not pfDatabase or not pfDatabase.SearchMobID or not pfDatabase.SearchObjectID then
return
end
for playerName, targets in pairs(partyQuestData) do
for targetKey, quests in pairs(targets) do
for _, data in ipairs(quests) do
if data.targetType and data.targetId and data.questId
and (data.current or 0) < (data.total or 0)
and not LocalPlayerHasObjective(targetKey, data.quest, data.objective) then
-- BuildQuestDescription (called by SearchMobID/SearchObjectID's
-- AddNode) needs QTYPE to produce anything at all -- without
-- it, the route arrow's description line stays blank. Mirror
-- the HDB path's phase/origin-based QTYPE selection here too.
local qtype, item
if data.itemId then
qtype = "ITEM_OBJECTIVE_LOOT"
item = pfDB["items"] and pfDB["items"]["enUS"] and pfDB["items"]["enUS"][data.itemId]
elseif data.targetType == "O" then
qtype = "OBJECT_OBJECTIVE"
else
qtype = "UNIT_OBJECTIVE"
end
local meta = {
addon = "PFPARTY",
quest = data.quest,
questid = data.questId,
QTYPE = qtype,
item = item,
}
if data.targetType == "U" then
pfDatabase:SearchMobID(data.targetId, meta)
elseif data.targetType == "O" then
pfDatabase:SearchObjectID(data.targetId, meta)
end
end
end
end
end
end
-- Incoming party sync data can arrive as many addon messages in one burst.
-- Draw once after the burst instead of deleting and rebuilding the party pin
-- layer for every message.
local partyPinRefresh = CreateFrame("Frame")
local function QueuePartyPinRender()
partyPinRefresh.elapsed = 0
partyPinRefresh:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= 0.10 then
this:SetScript("OnUpdate", nil)
RenderPartyQuestPins()
end
end)
end
-- Config apply must not depend on receiving another party/quest event.
function pfMap:ApplyPartySettings(changes)
if changes.showPartyQuestPins then
partyPinRefresh:SetScript("OnUpdate", nil)
RenderPartyQuestPins()
if pfQuest_config.showPartyQuestPins ~= "1" then
for frame in pairs(self.highlightdb or {}) do
if frame.addon == "PFPARTY" then frame:Hide() end
end
end
end
if pfQuest.route then pfQuest.route:Reset() end
self.lastRouteMap = nil
self.queue_update = GetTime()
end
local function GetClassColor(playerName)
if playerName == UnitName("player") then
local _, class = UnitClass("player")
if class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
local classColor = RAID_CLASS_COLORS[class]
return {classColor.r, classColor.g, classColor.b}
end
end
for i = 1, GetNumPartyMembers() do
local name = UnitName("party" .. i)
if name == playerName then
local _, class = UnitClass("party" .. i)
if class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
local classColor = RAID_CLASS_COLORS[class]
return {classColor.r, classColor.g, classColor.b}
end
break
end
end
return {1.0, 1.0, 1.0}
end
local function BuildQuestGroups(matchedKey)
local questGroups = {}
local localPlayerName = UnitName("player")
for playerName, targets in pairs(partyQuestData) do
if playerName ~= localPlayerName and targets[matchedKey] then
for _, data in ipairs(targets[matchedKey]) do
questGroups[data.quest] = questGroups[data.quest] or {}
local isDuplicate = false
for _, existing in ipairs(questGroups[data.quest]) do
if existing.player == playerName and existing.objective == data.objective then
existing.current = data.current
existing.total = data.total
isDuplicate = true
break
end
end
if not isDuplicate then
table.insert(questGroups[data.quest], {
player = playerName,
objective = data.objective,
current = data.current,
total = data.total
})
end
end
end
end
return questGroups
end
local function AddQuestGroupLines(tooltip, questGroups, shouldSkipHeader)
tooltip:AddLine(" ")
for questName, lines in pairs(questGroups) do
if not (shouldSkipHeader and shouldSkipHeader(questName)) then
local symbol = "|cff555555[|cffffcc00!|cff555555]|r "
tooltip:AddLine(symbol .. questName, 1, 1, 0)
end
for _, line in ipairs(lines) do
local classColor = GetClassColor(line.player)
local perc = line.current / line.total
local r, g, b
if perc <= 0.5 then
perc = perc * 2
r, g, b = 1, perc, 0
else
perc = perc * 2 - 1
r, g, b = 1 - perc, 1, 0
end
local cr, cg, cb = classColor[1] * 255, classColor[2] * 255, classColor[3] * 255
local coloredName = string.format("|cFF%02x%02x%02x%s|r", cr, cg, cb, line.player)
local displayText = string.format("|cffaaaaaa- |r%s: %s (%d/%d)", coloredName, line.objective, line.current, line.total)
tooltip:AddLine(displayText, r, g, b)
end
end
tooltip:AddLine(" ")
end
local function HookGameTooltip()
local watcher = CreateFrame("Frame", nil, GameTooltip)
watcher:SetScript("OnShow", function()
local unitName = UnitName("mouseover")
if not unitName then return end
if UnitIsPlayer("mouseover") then DebugPrint("HookGameTooltip: skipped, mouseover is a player") return end
local featureEnabled = pfQuest_config and pfQuest_config["showPartyProgress"] == "1"
if not featureEnabled then DebugPrint("HookGameTooltip: skipped, showPartyProgress disabled") return end
local hasData = false
for _, targets in pairs(partyQuestData) do
if targets[unitName] then
hasData = true
break
end
end
DebugPrint("HookGameTooltip: unit=" .. unitName .. " hasData=" .. tostring(hasData))
if not hasData then return end
AddQuestGroupLines(GameTooltip, BuildQuestGroups(unitName), function(questName)
for qid = 1, GetNumQuestLogEntries() do
if pfQuestCompat.GetQuestLogTitle(qid) == questName then
return true
end
end
return false
end)
GameTooltip:Show()
end)
end
local function HookPfQuestTooltip()
if not pfMap or not pfMap.ShowTooltip or not pfQuestCompat then
return false
end
local orig_ShowTooltip = pfMap.ShowTooltip
pfMap.ShowTooltip = function(self, meta, tooltip)
tooltip = tooltip or GameTooltip
local targetKey = meta.spawn or meta.title
local tooltipName = nil
local tooltipText = getglobal("GameTooltipTextLeft1")
if tooltipText and tooltipText:GetText() then
tooltipName = tooltipText:GetText()
end
local inParty = GetNumPartyMembers() > 0
local featureEnabled = pfQuest_config and pfQuest_config["showPartyProgress"] == "1"
DebugPrint("ShowTooltip: targetKey=" .. tostring(targetKey) .. " tooltipName=" .. tostring(tooltipName) .. " metaQuest=" .. tostring(meta["quest"]))
if meta["quest"] and inParty and featureEnabled and targetKey then
for qid = 1, GetNumQuestLogEntries() do
local qtitle, _, _, _, _, complete = pfQuestCompat.GetQuestLogTitle(qid)
if meta["quest"] == qtitle then
local objectives = tonumber(GetNumQuestLeaderBoards(qid)) or 0
if objectives > 0 then
for i = 1, objectives do
local text = GetQuestLogLeaderBoard(i, qid)
if text then
local _, _, objName, current, total = string.find(text, "(.*):%s*(%d+)%s*/%s*(%d+)")
if objName and current and total then
objName = string.gsub(objName, "^%s*(.-)%s*$", "%1")
if string.len(objName) > 0 then
CaptureMyQuestData(targetKey, qtitle, objName, tonumber(current), tonumber(total))
if tooltipName and tooltipName ~= targetKey then
CaptureMyQuestData(tooltipName, qtitle, objName, tonumber(current), tonumber(total))
end
end
end
end
end
end
end
end
end
local hasPartyData, matchedKey = false, nil
if featureEnabled then
if targetKey then
for _, targets in pairs(partyQuestData) do
if targets[targetKey] then
hasPartyData, matchedKey = true, targetKey
break
end
end
end
if not hasPartyData and tooltipName then
for _, targets in pairs(partyQuestData) do
if targets[tooltipName] then
hasPartyData, matchedKey = true, tooltipName
break
end
end
end
end
DebugPrint("ShowTooltip: hasPartyData=" .. tostring(hasPartyData) .. " matchedKey=" .. tostring(matchedKey) .. " mouseoverExists=" .. tostring(UnitExists("mouseover")))
if hasPartyData and matchedKey and not UnitExists("mouseover") then
local oldquest = meta["quest"]
meta["quest"] = nil
local ok = pcall(orig_ShowTooltip, self, meta, tooltip)
meta["quest"] = oldquest
if not ok then
DebugPrint("orig_ShowTooltip failed while suppressing quest line")
end
AddQuestGroupLines(tooltip, BuildQuestGroups(matchedKey))
tooltip:Show()
else
orig_ShowTooltip(self, meta, tooltip)
end
end
return true
end
-- QUEST_LOG_UPDATE arrives in bursts during login. Rebuilding mappings walks
-- the active log and database relations, so coalesce those bursts into one
-- pass after the client has settled.
local mappingRefresh = CreateFrame("Frame")
local function QueueMappingRefresh(forceFullSync, requestPeers)
mappingRefresh.forceFullSync = mappingRefresh.forceFullSync or forceFullSync
mappingRefresh.requestPeers = mappingRefresh.requestPeers or requestPeers
mappingRefresh.elapsed = 0
mappingRefresh:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= 0.5 then
local force = this.forceFullSync
local request = this.requestPeers
this.forceFullSync = nil
this.requestPeers = nil
this:SetScript("OnUpdate", nil)
if GetNumPartyMembers() > 0 then
RebuildQuestMappings()
if request then
SendAddonMessage("PFQT_SYNC", "1", "PARTY")
end
ShareQuestData(force)
-- myQuestMappings just changed, which is what
-- LocalPlayerHasObjective reads: re-evaluate now rather than
-- waiting for the next unrelated trigger, so a party star
-- drops out (or appears) as soon as the local player's own
-- quest state actually does.
QueuePartyPinRender()
end
end
end)
end
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("CHAT_MSG_ADDON")
eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
eventFrame:RegisterEvent("QUEST_LOG_UPDATE")
eventFrame:SetScript("OnEvent", function()
if event == "CHAT_MSG_ADDON" then
local prefix, message, channel, sender = arg1, arg2, arg3, arg4
if prefix == "pfqt" then
ProcessQuestData(sender, message)
QueuePartyPinRender()
elseif prefix == "PFQT_SYNC" then
if GetNumPartyMembers() > 0 and sender ~= UnitName("player") then
QueueMappingRefresh(true)
end
end
elseif event == "PARTY_MEMBERS_CHANGED" then
CleanupPartyData()
QueuePartyPinRender()
if GetNumPartyMembers() > 0 then
QueueMappingRefresh(true, true)
end
elseif event == "QUEST_LOG_UPDATE" then
if GetNumPartyMembers() > 0 then
QueueMappingRefresh()
-- Piggyback on this frequently-firing event to pick up a config
-- checkbox toggle without a dedicated polling ticker. Cheap: a
-- full clear-and-rebuild over what is normally a handful of
-- party objective entries.
QueuePartyPinRender()
end
end
end)
local function ExtendPfQuestConfig()
for _, entry in pairs(pfQuest_defconfig) do
if entry.config == "showPartyProgress" then
return
end
end
table.insert(pfQuest_defconfig, {
text = "Party Quest Sharing",
type = "header",
page = "features"
})
table.insert(pfQuest_defconfig, {
text = "Show Party Quest Progress on Tooltips",
default = "1",
type = "checkbox",
config = "showPartyProgress"
})
table.insert(pfQuest_defconfig, {
text = "Show Party Members' Quest Objectives on Map |cffffcc00[Beta]|r",
default = "0",
type = "checkbox",
config = "showPartyQuestPins",
tooltip = "This feature is still being tested. Party members' active quest objectives are shown as a separate colored marker on the map. Behavior may change or have rough edges."
})
table.insert(pfQuest_defconfig, {
text = "Allow Routing to Party Members' Quest Objectives |cffffcc00[Beta]|r",
default = "0",
type = "checkbox",
config = "showPartyQuestPinsRoutable",
tooltip = "This feature is still being tested. When enabled, the navigation arrow can target a party member's quest objective marker, not just your own."
})
pfQuest_config["showPartyProgress"] = pfQuest_config["showPartyProgress"] or "1"
pfQuest_config["showPartyQuestPins"] = pfQuest_config["showPartyQuestPins"] or "0"
pfQuest_config["showPartyQuestPinsRoutable"] = pfQuest_config["showPartyQuestPinsRoutable"] or "0"
end
local configExtenderFrame = CreateFrame("Frame")
configExtenderFrame:RegisterEvent("VARIABLES_LOADED")
configExtenderFrame:SetScript("OnEvent", function()
ExtendPfQuestConfig()
HookGameTooltip()
RenderPartyQuestPins()
if GetNumPartyMembers() > 0 then
QueueMappingRefresh(true, true)
end
local timer = 0
local rebuildRetries = 0
this:SetScript("OnUpdate", function()
timer = timer + 1
if rebuildRetries < 50 and (not pfDB or not pfDB["quests"] or not pfDB["quests"]["data"]) then
if math.mod(timer, 5) == 0 then
rebuildRetries = rebuildRetries + 1
if pfDB and pfDB["quests"] and pfDB["quests"]["data"] then
RebuildQuestMappings()
if GetNumPartyMembers() > 0 then
SendAddonMessage("PFQT_SYNC", "1", "PARTY")
ShareQuestData(true)
end
RenderPartyQuestPins()
end
end
end
if timer > 10 then
if HookPfQuestTooltip() then
this:SetScript("OnUpdate", nil)
this:UnregisterAllEvents()
elseif timer > 300 then
this:SetScript("OnUpdate", nil)
this:UnregisterAllEvents()
end
end
end)
end)
if pfQuest_defconfig and pfQuest_config then ExtendPfQuestConfig() end
SLASH_PFQUESTDEBUG1 = "/pfqd"
SlashCmdList["PFQUESTDEBUG"] = function(msg)
local lowerMsg = msg and string.lower(msg)
if lowerMsg == "on" then
PARTYPROGRESS_DEBUG = true
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Party progress debug logging ON - hover the mob to see what's happening")
return
elseif lowerMsg == "off" then
PARTYPROGRESS_DEBUG = false
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Party progress debug logging OFF")
return
end
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle Party Progress Debug:|r")
DEFAULT_CHAT_FRAME:AddMessage("Party members: " .. GetNumPartyMembers())
DEFAULT_CHAT_FRAME:AddMessage("Tooltip progress enabled: " .. tostring(pfQuest_config and pfQuest_config["showPartyProgress"] == "1"))
DEFAULT_CHAT_FRAME:AddMessage("Map pins enabled: " .. tostring(pfQuest_config and pfQuest_config["showPartyQuestPins"] == "1")
.. " (routable: " .. tostring(not pfQuest_config or pfQuest_config["showPartyQuestPinsRoutable"] ~= "0") .. ")")
local count = 0
for _ in pairs(myQuestMappings) do count = count + 1 end
DEFAULT_CHAT_FRAME:AddMessage("myQuestMappings targets: " .. count)
for targetKey, quests in pairs(myQuestMappings) do
DEFAULT_CHAT_FRAME:AddMessage(" Target: " .. targetKey)
for _, data in ipairs(quests) do
DEFAULT_CHAT_FRAME:AddMessage(" - " .. data.quest .. ": " .. data.objective .. " (" .. data.current .. "/" .. data.total .. ")")
end
end
local partyCount = 0
local pinnableCount = 0
for _ in pairs(partyQuestData) do partyCount = partyCount + 1 end
DEFAULT_CHAT_FRAME:AddMessage("partyQuestData players: " .. partyCount)
for playerName, targets in pairs(partyQuestData) do
DEFAULT_CHAT_FRAME:AddMessage(" Player: " .. playerName)
for targetKey, quests in pairs(targets) do
DEFAULT_CHAT_FRAME:AddMessage(" Target: " .. targetKey)
for _, data in ipairs(quests) do
local pinnable = data.targetType and data.targetId and data.questId
and (data.current or 0) < (data.total or 0)
if pinnable then pinnableCount = pinnableCount + 1 end
DEFAULT_CHAT_FRAME:AddMessage(" - " .. data.quest .. ": " .. data.objective .. " (" .. data.current .. "/" .. data.total .. ")"
.. (pinnable and " |cff33ffcc[pinnable]|r" or ""))
end
end
end
DEFAULT_CHAT_FRAME:AddMessage("Pinnable party entries: " .. pinnableCount)
if count == 0 and partyCount == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000No quest data found!|r Try killing a quest mob, or run /pfqrebuild after joining a party.")
end
end
SLASH_PFQUEREBUILD1 = "/pfqrebuild"
SlashCmdList["PFQUEREBUILD"] = function(msg)
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Rebuilding quest mappings...")
RebuildQuestMappings()
if GetNumPartyMembers() > 0 then
SendAddonMessage("PFQT_SYNC", "1", "PARTY")
ShareQuestData(true)
end
RenderPartyQuestPins()
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Done! Run /pfqd to see results.")
end
SLASH_PFQUESTTEST1 = "/pfqtest"
SlashCmdList["PFQUESTTEST"] = function(msg)
if not msg or msg == "" then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Usage: /pfqtest <exact unit name> - tooltip-only test, then mouseover that unit")
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Usage: /pfqtest <exact unit name> <unitId> <questId> - also drops a test map pin, real IDs required")
return
end
local unitName, unitId, questId = string.match(msg, "^(.-)%s+(%d+)%s+(%d+)$")
partyQuestData["TestBuddy"] = partyQuestData["TestBuddy"] or {}
if unitName and unitId and questId then
unitId = tonumber(unitId)
questId = tonumber(questId)
partyQuestData["TestBuddy"][unitName] = {
{
quest = "Test Quest",
objective = "Kill " .. unitName,
current = 3,
total = 10,
questId = questId,
targetId = unitId,
targetType = "U"
}
}
RenderPartyQuestPins()
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Injected fake party progress + pin for '" .. unitName
.. "' (unit " .. unitId .. ", quest " .. questId .. ") from TestBuddy. Requires showPartyQuestPins enabled and unit "
.. unitId .. " to have known spawn coordinates.")
else
partyQuestData["TestBuddy"][msg] = {
{
quest = "Test Quest",
objective = "Kill " .. msg,
current = 3,
total = 10
}
}
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfQuest-turtle:|r Injected fake party progress for '" .. msg .. "' from TestBuddy. Mouseover it to check the tooltip.")
end
end