diff --git a/Omen.lua b/Omen.lua index 1f6a2fa..262f12b 100644 --- a/Omen.lua +++ b/Omen.lua @@ -1378,6 +1378,19 @@ local function updatethreat(unitid, mobunitid) local guid = UnitGUID(unitid) if guid and not threatTable[guid] then local isTanking, state, scaledPercent, rawPercent, threatValue = UnitDetailedThreatSituation(unitid, mobunitid) + -- CoA: the Vol'jin/CoA core does not push party-member threat to the + -- client, so the API call above returns nil for everyone but us. Fall + -- back to values broadcast by peers running OmenSync. + if not threatValue and Omen.SyncGetThreat then + local mobGUID = UnitGUID(mobunitid) + if mobGUID then + local syncVal, syncTanking = Omen:SyncGetThreat(guid, mobGUID) + if syncVal then + threatValue = syncVal + isTanking = syncTanking + end + end + end if threatValue then -- Threat can be negative due to temporary threat reduction effects such as Fade and Mirror Image (-410065408). if threatValue < 0 then @@ -1387,6 +1400,14 @@ local function updatethreat(unitid, mobunitid) if threatValue > topthreat then topthreat = threatValue end if isTanking then tankGUID = guid end threatTable[guid] = threatValue + -- CoA: broadcast our own player/pet threat so peers' OmenSync can + -- fill the gap left by their nil UnitDetailedThreatSituation call. + if (unitid == "player" or unitid == "pet") and Omen.SyncBroadcastThreat then + local mobGUID = UnitGUID(mobunitid) + if mobGUID then + Omen:SyncBroadcastThreat(guid, mobGUID, threatValue, isTanking) + end + end else -- We use the special value -1 to indicate nil here. threatTable[guid] = -1 diff --git a/Omen.toc b/Omen.toc index c71e7ce..aade904 100644 --- a/Omen.toc +++ b/Omen.toc @@ -55,3 +55,8 @@ Localization\ruRU.lua CoAClassColors.lua Omen.lua + +# Loaded after Omen.lua so the AceAddon namespace exists and we can +# attach Sync* methods. Provides addon-channel fallback for the threat +# data Vol'jin/CoA does not push for party members. +OmenSync.lua diff --git a/OmenSync.lua b/OmenSync.lua new file mode 100644 index 0000000..40de513 --- /dev/null +++ b/OmenSync.lua @@ -0,0 +1,141 @@ +-- OmenSync.lua +-- +-- Addon-channel sync for party/raid threat values. +-- +-- Why this exists +-- --------------- +-- Omen reads threat data via UnitDetailedThreatSituation(unit, mob). +-- That API only returns data the server pushes to the client. On the +-- Conquest of Azeroth realm (Vol'jin server) party-member threat is +-- not pushed — UnitDetailedThreatSituation("party1", "target") +-- returns nil even mid-combat, so Omen draws only the local player's +-- bar. The same Omen install on Bronzebeard (classic+) shows the +-- whole party because that core does push the data. +-- +-- This module fills the gap by sending each player's own player+pet +-- threat over SendAddonMessage("OMSYNC", …, "PARTY"|"RAID"). Receivers +-- store incoming values keyed by senderGUID and serve them to +-- updatethreat() in Omen.lua as a fallback whenever the client API +-- returns nil. When the server *does* push data (Bronzebeard) the +-- API path wins and the sync values are simply unused — making this +-- file a no-op on healthy realms. +-- +-- Wire format (pipe-separated, terse to stay under the 255-byte +-- AddonMessage payload cap): +-- ||| +-- The sender's own name comes from CHAT_MSG_ADDON's `sender` arg, so +-- it doesn't need to be in the message; we only need the subject GUID +-- because each player broadcasts both their player and pet GUIDs. +-- +-- Both peers must run this fork (or any addon that emits/consumes the +-- "OMSYNC" prefix). Other Omen installs ignore the prefix silently. + +local Omen = LibStub("AceAddon-3.0"):GetAddon("Omen") +if not Omen then return end + +local PREFIX = "OMSYNC" +local THROTTLE = 0.4 -- min seconds between sends per (subject, mob) +local STALE = 8 -- seconds; entries older than this are ignored +local MIN_DELTA = 0.05 -- 5%; smaller changes don't trigger a send + +-- incomingThreat[subjectGUID][mobGUID] = { value, isTanking, time } +local incomingThreat = {} +-- lastSend[subjectGUID][mobGUID] = { value, time } +local lastSend = {} + +local function nowGetTime() return GetTime() end + +local function packMsg(subject, mob, value, isTanking) + return string.format("%s|%s|%d|%d", subject, mob, value, isTanking and 1 or 0) +end + +local function unpackMsg(msg) + local subject, mob, val, tank = string.match(msg, "^([^|]+)|([^|]+)|(%-?%d+)|([01])$") + if not subject then return nil end + return subject, mob, tonumber(val), tank == "1" +end + +local function inGroup() + return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 +end + +local function pruneOlderThan(t, limit) + for k, v in pairs(t) do + if v.time and v.time < limit then t[k] = nil end + end +end + +-- --------------------------------------------------------------------------- +-- Public API: called from Omen.lua's local updatethreat() +-- --------------------------------------------------------------------------- + +-- Look up the most recent synced threat value for `subjectGUID` on +-- `mobGUID`, or nil if we don't have a fresh entry. Returns +-- (value, isTanking). +function Omen:SyncGetThreat(subjectGUID, mobGUID) + local byMob = incomingThreat[subjectGUID] + if not byMob then return nil end + local entry = byMob[mobGUID] + if not entry then return nil end + if nowGetTime() - entry.time > STALE then + byMob[mobGUID] = nil + return nil + end + return entry.value, entry.isTanking +end + +-- Broadcast our own (player or pet) threat to the party/raid. Throttled +-- per (subject, mob); silently no-ops outside groups. +function Omen:SyncBroadcastThreat(subjectGUID, mobGUID, value, isTanking) + if not inGroup() then return end + if not subjectGUID or not mobGUID or not value then return end + if value < 0 then return end -- ignore the temporary-negative encoding + + lastSend[subjectGUID] = lastSend[subjectGUID] or {} + local prev = lastSend[subjectGUID][mobGUID] + local now = nowGetTime() + if prev then + local age = now - prev.time + local maxV = math.max(value, prev.value, 1) + local pct = math.abs(value - prev.value) / maxV + if age < THROTTLE and pct < MIN_DELTA then return end + end + lastSend[subjectGUID][mobGUID] = { value = value, time = now } + + local channel = GetNumRaidMembers() > 0 and "RAID" or "PARTY" + SendAddonMessage(PREFIX, packMsg(subjectGUID, mobGUID, value, isTanking), channel) +end + +-- --------------------------------------------------------------------------- +-- Receiver +-- --------------------------------------------------------------------------- + +local f = CreateFrame("Frame") +f:RegisterEvent("CHAT_MSG_ADDON") +f:RegisterEvent("PLAYER_LEAVING_WORLD") +f:RegisterEvent("PARTY_MEMBERS_CHANGED") +f:RegisterEvent("RAID_ROSTER_UPDATE") +f:SetScript("OnEvent", function(self, event, ...) + if event == "CHAT_MSG_ADDON" then + local prefix, msg, _, sender = ... + if prefix ~= PREFIX then return end + if sender == UnitName("player") then return end -- ignore self-echo + + local subject, mob, value, isTanking = unpackMsg(msg) + if not subject then return end + + local byMob = incomingThreat[subject] + if not byMob then + byMob = {} + incomingThreat[subject] = byMob + end + byMob[mob] = { value = value, isTanking = isTanking, time = nowGetTime() } + + -- Cheap GC: prune stale mob entries off the same subject. + pruneOlderThan(byMob, nowGetTime() - STALE) + else + -- World transitions / roster changes invalidate everything. + wipe(incomingThreat) + wipe(lastSend) + end +end)