diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d430fe..5d680d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ All notable changes to DopingControl. Versions are the ones actually released here — a new feature raises the middle number and resets the last one, the last number is for fixes without new features. +## v0.8.1 + +(0.8.0 was never released; its changes are part of this version.) + +### Addon check: raiders answer through shootyepgp + +* Every addon check (the **Check addons** button and the automatic check when + the tab opens) now also sends one `ADDONCHECK` query: `Q1::` + for BigWigs, TWThreat, shootyepgp, UpsiRinse, Rinse, RABuffs and + BetterCharacterStats. shootyepgp builds with the responder answer + `A1::=L/;=D;=M` (loaded with the + version from its `.toc`, installed but disabled, not installed), split into + messages of at most 240 characters. +* An answer is certain and beats every passive rule and class rule in its + column: green with the version when loaded, red when not installed or + disabled (the tooltip says which). Rinse counts as loaded if either Rinse + or UpsiRinse is. +* Versions from answers are compared across the raid like every other + version, so an older one gets the "!" mark. +* Raiders without the responder are read exactly as before; silence is never + turned into red. + +### Own row + +* Your own row shows an addon you have installed even where the class rule + would call it not expected (Rinse on a class that cannot dispel). + +### Addons tab: RABuffs and BetterCharacterStats + +Two more columns on the Addons tab. Without an addon-check answer both are +detected **passively** (nothing is sent for them, no visible chat, no +whispers): + +* **RAB (RABuffs).** Any `RAB/BT` broadcast (every 30 s per client, only when + the member has mana or a tracked timed buff) marks the raider as present; + the version only comes from `RAB/V`/`RAB/VC`, which RABuffs has disabled + upstream but are parsed if they ever arrive. After 70 s in the raid without + any message the cell shows grey "Not seen" (never red); the tooltip says + this is weak evidence. +* **BCS (BetterCharacterStats).** A `bcs` message starting with `TREE,` marks + the sending druid as present. Everyone not heard from and without an + addon-check answer stays unknown, never missing and never n/a (the message + only reaches the sender's own subgroup, and only in Tree of Life form). +* The addon keeps track of when each raider first appeared in the roster for + the 70 s rule. Your own row reads RABuffs and BetterCharacterStats from your + own addon list. `/dc test` shows both columns. +* The tab has two more columns, so the matrix is wider on this tab. + ## v0.7.0 ### New Addons tab: who in the raid runs BigWigs, Threat Meter, EPGP, SuperWoW, Rinse diff --git a/DopingControl.toc b/DopingControl.toc index 7816cb6..6de0c4c 100644 --- a/DopingControl.toc +++ b/DopingControl.toc @@ -2,7 +2,7 @@ ## Title: DopingControl ## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker ## Author: ShempError -## Version: 0.7.0 +## Version: 0.8.1 ## SavedVariables: DopingControlDB core\const.lua diff --git a/README.md b/README.md index 6b8b74c..9219a85 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,18 @@ without it. in the cells, ready pills per role group, gaps first, unreadable players marked instead of blamed.* +## What's new — v0.8.1 · 2026-10-18 + +- **The Addons tab checks two more addons:** **RABuffs** (RAB) and **BetterCharacterStats** + (BCS), from what those addons already broadcast. See [The Addons tab](#the-addons-tab). +- **Raiders can answer for themselves.** DopingControl now also sends one **addon check** that + shootyepgp builds with the matching responder answer: per addon *loaded* (with its version), + *installed but disabled* or *not installed*. Such an answer is certain and wins over every + passive rule, so BCS on a warrior, RABuffs on a rogue or the original Rinse become visible. + Raiders without it are read as before and are never shown red for staying silent. +- **Your own row** shows an addon you have installed even where the class rule says it is not + expected (Rinse on a warrior, for example). + ## What's new — v0.7.0 · 2026-10-15 - **A new Addons tab** shows which raid addons the other raiders run — BigWigs, Threat Meter, @@ -35,6 +47,7 @@ blamed.* **Version history** — details in [CHANGELOG.md](CHANGELOG.md): +- **v0.8.0** — the Addons tab also checks RABuffs and BetterCharacterStats (passive only). - **v0.7.0** — the **Addons tab** (BigWigs, Threat Meter, EPGP, SuperWoW, Rinse across the raid); the Equipment tooltip names the enchant. - **v0.6.4** — the equipment expectation top-up self-heals on every refresh, so a profile @@ -180,14 +193,22 @@ a version with `!`, a red missing cell, a grey `?` for an unreadable player and addon is not expected.* The question this tab answers is **"who in my raid runs the addons I rely on?"** — BigWigs, -Threat Meter (TW Threatmeter), EPGP (shootyepgp), SuperWoW and Rinse. There are no +Threat Meter (TW Threatmeter), EPGP (shootyepgp), SuperWoW, Rinse, RABuffs and +BetterCharacterStats. There are no expectations to set and nothing to whisper; it is a display tab like Resistances and Hit. **How it checks.** The other raiders do **not** need DopingControl. Each column is detected only through what that addon itself sends or answers: BigWigs' own version query, the Threat Meter "who" query, shootyepgp's version announcement (and its SuperWoW reply), and the UpsiRinse version query. DopingControl sends the queries those addons already understand and reads the -answers. +answers. RABuffs and BetterCharacterStats have no usable query, so for them DopingControl +listens to what they broadcast on their own. + +On top of that, every check sends one **addon check** (prefix `ADDONCHECK`) for the whole list. +A raider whose shootyepgp carries the responder answers it per addon folder: loaded (with the +version from its `.toc`), installed but disabled, or not installed. That answer is **certain** +and beats every passive rule and class rule in its column (BW, THR, EPG, RIN, RAB, BCS). A +raider whose shootyepgp does not answer it is read the old way. - The **Check addons** button sends one burst of queries and collects replies for six seconds. The status line next to it counts the window down. @@ -203,8 +224,8 @@ answers. |---|---| | Green, version, check mark | The addon is there and this is the **newest version in the raid** (your own row counts). | | Green, version, "!" | The addon is there but **older than the newest** version seen, or the version cannot be compared. The tooltip names the newest. | -| Red | **No reply** after the query window passed. | -| Grey "?" | **Not asked yet** (or the player is offline). Silence before any query is never shown as red. | +| Red | **No reply** after the query window passed, or the raider's addon check reported the addon as **not installed** or **installed but disabled**. | +| Grey "?" | **Not asked yet** (or the player is offline). Silence before any query is never shown as red. The tooltip says "Not seen" for RABuffs silence and explains the other unknown cases. | | Dot | **Not applicable** — see the limits below. | Hovering a cell shows the version, the raw reply, which fork answered and how long ago it was @@ -223,8 +244,26 @@ a red cell says exactly that, and each column header carries its own limits: - **SW (SuperWoW)** — also guild members running shootyepgp only: SuperWoW itself cannot be seen from outside, so shootyepgp answers for it. A raider outside your guild shows a dot, not red. - **RIN (Rinse)** — only dispellers (Druid, Priest, Paladin, Shaman, Mage) are checked; everyone - else shows a dot. Only the **UpsiRinse** fork answers the query, so a raider with the original - Rinse shows red even though the addon is installed. + else shows a dot unless their addon check reports Rinse loaded. Only the **UpsiRinse** fork + answers the version query, so a raider with the original Rinse shows red even though the + addon is installed — unless their addon check answers, which knows both forks. +- **RAB (RABuffs)** — the addon check if it answers, otherwise passive. Every RABuffs client in a raid broadcasts its buff data every + 30 seconds, and any such message from a raider proves the addon (no version; the disabled + upstream version messages are read if they ever arrive). A raider who has been in the raid + for at least **70 seconds** without a single message shows a grey "?" with the tooltip + **"Not seen"**, never red: this is **weak evidence**, because members without mana and + without a tracked timed buff send nothing at all. Before 70 seconds the cell is just unknown. +- **BCS (BetterCharacterStats)** — the addon check is the only reliable source. Without it, + the addon's only traffic is a message druids send on the **party** channel when their auras + change in **Tree of Life form**; hearing it proves the sender runs the addon. Everyone else + without an answer stays a grey "?", never red and never a dot: the addon is expected for + every class, it just cannot be seen from outside. + +**The addon check, for addon authors.** Prefix `ADDONCHECK`, sent on RAID, PARTY, GUILD or +BATTLEGROUND. Query: `Q1::,,...` with at most 16 folder names. Answer: +`A1::=L/;=D;=M` — `L` loaded (version from the `.toc`, `?` +if it has none), `D` installed but disabled, `M` not installed. Long answers are split into +several messages of at most 240 characters; each query id is answered once per sender. **Reading the totals.** Each class group's header counts the players, the gaps (red cells) and how many answered. The footer **MISSING PER ADDON** counts the red cells per column, and the diff --git a/core/const.lua b/core/const.lua index 20f0063..5a23d46 100644 --- a/core/const.lua +++ b/core/const.lua @@ -98,11 +98,30 @@ DC.SLOTS_ADDONS = { limits = "Guild members running shootyepgp only. The SuperWoW DLL" .. " itself cannot be detected." }, { id = "RINSE", label = "RIN", sub = "dispel", full = "Rinse", kind = "addon", - how = "Asks the group with the UpsiRinse version query; it answers" - .. " with its version.", + how = "Asks the group with the UpsiRinse version query and the" + .. " shootyepgp addon check. A shootyepgp answer is certain:" + .. " UpsiRinse or Rinse loaded = present, both disabled or" + .. " absent = missing.", limits = "Only dispellers (Druid, Priest, Paladin, Shaman, Mage) are" - .. " checked, and only the UpsiRinse fork answers - the original" - .. " Rinse does not." }, + .. " checked for a miss. Without a shootyepgp answer only the" + .. " UpsiRinse fork replies - the original Rinse does not." }, + { id = "RABUFFS", label = "RAB", sub = "buffs", full = "RABuffs", kind = "addon", + how = "Asks every shootyepgp client (addon check) whether RABuffs is" + .. " loaded, disabled or not installed - that answer is certain." + .. " Also passive: RABuffs clients in a raid broadcast buff data" + .. " every 30 s, and any such message proves the addon.", + limits = "Players without a current shootyepgp (or outside your" + .. " guild) do not answer. Then silence is weak evidence only:" + .. " members without mana and tracked timed buffs send nothing," + .. " so 'not seen' (grey) appears after 70 s in the raid and" + .. " never means a confirmed miss." }, + { id = "BCS", label = "BCS", sub = "stats", full = "BetterCharacterStats", kind = "addon", + how = "Asks every shootyepgp client (addon check) whether" + .. " BetterCharacterStats is loaded, disabled or not installed -" + .. " that answer is certain, with its version. Also passive: a" + .. " druid's Tree of Life message on PARTY proves the addon.", + limits = "Players without a current shootyepgp (or outside your" + .. " guild) do not answer; they stay unknown, never missing." }, } -- ------------------------------------------------------------------ diff --git a/core/model.lua b/core/model.lua index dbce45a..aaf8851 100644 --- a/core/model.lua +++ b/core/model.lua @@ -583,7 +583,8 @@ function M.classify(player, slotdef, expected, books) if kind == "addon" then -- ADDONS tab: the cell is the evaluated reply scan/addons.lua put -- on player.addons[slot] ({ status = ok | old | missing | na | - -- unknown, text, mark, ... }). Silence is NEVER missing on its own: + -- unknown | notseen, text, mark, ... }; "notseen" (RABuffs silence) + -- stays UNKNOWN here, never a confirmed miss). Silence is NEVER missing on its own: -- scan/addons.lua only says "missing" after a query window passed -- for a member who was in the group when the query went out, or -- when the addon is known absent (own row, SuperWoW answer "0"). diff --git a/core/sim.lua b/core/sim.lua index 03bf603..aa88e89 100644 --- a/core/sim.lua +++ b/core/sim.lua @@ -534,6 +534,18 @@ local function simAddons(i) else m.RINSE = { text = "3.0" } end + -- RABuffs: passive; false = "not seen" (grey), nil = too early + if math.mod(i, 6) == 1 then + m.RABUFFS = false + elseif math.mod(i, 7) == 2 then + m.RABUFFS = nil + else + m.RABUFFS = { text = "yes", presenceOnly = true } + end + -- BCS: only druids can show it; the rest of the sim stays unknown + if math.mod(i, 4) == 1 then + m.BCS = { text = "yes", presenceOnly = true } + end return m end diff --git a/scan/addons.lua b/scan/addons.lua index ffc45ad..99be1c4 100644 --- a/scan/addons.lua +++ b/scan/addons.lua @@ -17,6 +17,25 @@ -- proves shootyepgp itself is installed. -- Rinse prefix "Rinse": query "REPORT_ADDON_VERSION", reply -- "V_" (only the UpsiRinse fork answers). +-- RABuffs passive only. Every client in a raid broadcasts on prefix +-- "RAB/BT" (RAID) every 30 s: " " pairs ("h2oc 20 +-- fort 1500 ..."), but only when it has something to report +-- (mana users, tracked timed buffs). The version messages +-- "RAB/V" (" ") and "RAB/VC" are disabled upstream; +-- parsed if they ever arrive, never sent. Its own version check +-- runs over visible chat and is never used. Silence is weak +-- evidence: "not seen" only after RAB_SILENT seconds in the raid. +-- BCS BetterCharacterStats, passive only. Its sole traffic is prefix +-- "bcs" on PARTY: "TREE,[,]", sent by druids in +-- Tree of Life form. Too rare to prove anything: without a +-- responder answer (below) a member is unknown, never missing. +-- ADDONCHECK the shootyepgp responder. One query "Q1::" on +-- the group channel, every guild member's shootyepgp answers +-- "A1::=L/;=D;=M" (loaded / +-- installed but disabled / not installed), split over several +-- messages when long. That is certain evidence per folder and +-- beats every passive rule. Older shootyepgp versions and +-- non-guildmates stay silent: silence is never red for BCS/RAB. -- Honest-unknown rule: silence is only "no reply" after a query window has -- passed for a member who was in the group when the query went out; -- before that the cell is unknown, and members who cannot answer (not in @@ -33,9 +52,11 @@ local A = DC_Addons A.WINDOW = 6 -- seconds a query waits for replies A.AUTO_THROTTLE = 30 -- seconds between automatic queries A.MAX_NAMES = 400 -- passive-store cap (guild chatter from non-raiders) +A.RAB_SILENT = 70 -- seconds in the raid before RABuffs silence counts + -- (two 30 s broadcast cycles plus margin) -- slot ids (match DC.SLOTS_ADDONS) -A.SLOT_IDS = { "BIGWIGS", "TWT", "EPGP", "SUPERWOW", "RINSE" } +A.SLOT_IDS = { "BIGWIGS", "TWT", "EPGP", "SUPERWOW", "RINSE", "RABUFFS", "BCS" } A.DISPELLERS = { DRUID = true, PRIEST = true, PALADIN = true, SHAMAN = true, MAGE = true, @@ -48,13 +69,41 @@ local PREFIX_SLOT = { ["Rinse"] = "RINSE", ["SEPGP_PREFIX"] = "EPGP", ["SWOW_R"] = "SUPERWOW", + ["RAB/BT"] = "RABUFFS", + ["RAB/V"] = "RABUFFS", + ["RAB/VC"] = "RABUFFS", + ["bcs"] = "BCS", + ["ADDONCHECK"] = "CHECK", -- responder answers, own parser } A.PREFIXES = PREFIX_SLOT +-- ADDONCHECK protocol: folder name (as on the client) -> column +A.CHECK_PREFIX = "ADDONCHECK" +A.CHECK_NAMES = { "BigWigs", "TWThreat", "shootyepgp", "UpsiRinse", "Rinse", + "RABuffs", "BetterCharacterStats" } +local CHECK_SLOT = { + BigWigs = "BIGWIGS", TWThreat = "TWT", shootyepgp = "EPGP", + UpsiRinse = "RINSE", Rinse = "RINSE", RABuffs = "RABUFFS", + BetterCharacterStats = "BCS", +} +local CHECK_CHANNEL = { RAID = true, PARTY = true, GUILD = true, BATTLEGROUND = true } + -- passive store: A.data[name][slot] = entry A.data = {} A.dataCount = 0 +-- roster time: A.since[name] = when the name was first seen in the group +-- (feeds the RABuffs "not seen" rule) +A.since = {} + +-- responder store: A.resp[name][folder] = { s = "L"|"D"|"M", ver = text|nil } +-- (evicted with the roster in Prune, capped like the passive store) +A.resp = {} +A.respCount = 0 +A.checkId = nil -- id of the ADDONCHECK query in flight (answers to any + -- other id are ignored) +A.checkSeq = 0 -- survives Reset so ids never repeat inside a session + -- query state A.query = { at = nil, sent = {}, roster = {} } A.lastStatus = nil -- last StartQuery verdict text (shown next to the button) @@ -252,12 +301,49 @@ function A.ParseSwow(msg) return nil end +-- RABuffs. "RAB/V" payload " " (pattern RABuffs itself uses) +-- gives the version; a "RAB/BT" payload is " " pairs and only +-- proves presence. Anything else is ignored. +function A.ParseRABuffs(msg) + if type(msg) ~= "string" then + return nil + end + local _, _, v = string.find(msg, "^([%w.]+) %d+[.,]%d+%s*$") + v = A.Clean(v) + if v then + return entry(v, nil, msg) + end + if not string.find(msg, "^%a%w+ %d+") then + return nil + end + -- every byte must belong to a pair, else it is not a broadcast + local rest = string.gsub(msg, "%a%w+ %d+", "") + if string.find(rest, "%S") then + return nil + end + local e = entry("yes", nil, msg) + e.presenceOnly = true + return e +end + +-- BetterCharacterStats: only "TREE,[,]" exists +function A.ParseBCS(msg) + if type(msg) ~= "string" or not string.find(msg, "^TREE,") then + return nil + end + local e = entry("yes", nil, msg) + e.presenceOnly = true + return e +end + local PARSER = { BIGWIGS = A.ParseBigWigs, TWT = A.ParseTWT, RINSE = A.ParseRinse, EPGP = A.ParseEPGP, SUPERWOW = A.ParseSwow, + RABUFFS = A.ParseRABuffs, + BCS = A.ParseBCS, } -- ------------------------------------------------------------------ @@ -291,6 +377,9 @@ function A.OnAddonMessage(prefix, msg, channel, sender, now) if not slot or type(sender) ~= "string" or sender == A.own then return false end + if slot == "CHECK" then + return A.OnCheckAnswer(msg, channel, sender, now) + end -- BigWigs itself only honours its syncs on RAID if slot == "BIGWIGS" and channel ~= "RAID" then return false @@ -313,6 +402,115 @@ function A.OnAddonMessage(prefix, msg, channel, sender, now) return changed end +-- Build the query text for a fresh id (alphanumeric only) and remember it. +function A.NewCheckQuery(now) + A.checkSeq = A.checkSeq + 1 + local id = "d" .. string.format("%x", math.mod(math.floor((now or 0) * 1000), 1048576)) + .. "x" .. string.format("%x", A.checkSeq) + A.checkId = id + return "Q1:" .. id .. ":" .. table.concat(A.CHECK_NAMES, ",") +end + +-- Parse "A1::=L/;=D;..." -> id, list of +-- { folder, s, ver } or nil. Strict: one malformed part drops the message. +-- Folders we never asked about are skipped. +function A.ParseCheckAnswer(msg) + if type(msg) ~= "string" or string.len(msg) > 255 then + return nil + end + local _, _, id, list = string.find(msg, "^A1:(%w+):(.+)$") + if not id then + return nil + end + local out = {} + for part in string.gfind(list, "([^;]+)") do + local _, _, folder, st, rest = string.find(part, "^([%w_%-%.]+)=([LDM])(.*)$") + if not folder then + return nil + end + local ver + if st == "L" then + local _, _, v = string.find(rest, "^/(.*)$") + if not v then + return nil + end + v = A.Clean(v) + if v and v ~= "?" then + ver = v + end + elseif rest ~= "" then + return nil + end + if CHECK_SLOT[folder] then + table.insert(out, { folder = folder, s = st, ver = ver }) + end + end + return id, out +end + +-- responder answer -> store. Returns changed (bool). +function A.OnCheckAnswer(msg, channel, sender, now) + if not CHECK_CHANNEL[channel] or not A.checkId then + return false + end + local id, list = A.ParseCheckAnswer(msg) + if not id or id ~= A.checkId or table.getn(list) == 0 then + return false + end + local byName = A.resp[sender] + if not byName then + if A.respCount >= A.MAX_NAMES then + A.resp = {} + A.respCount = 0 + end + byName = {} + A.resp[sender] = byName + A.respCount = A.respCount + 1 + end + for i = 1, table.getn(list) do + local r = list[i] + byName[r.folder] = { s = r.s, ver = r.ver, at = now } + end + return true +end + +-- Responder verdict for one cell: nil (no answer) | { present = entry } | +-- { missing = why }. Rinse counts present when either fork is loaded and +-- missing only when both forks answered D/M. +function A.ResponderState(name, slot) + local byName = A.resp[name] + if not byName then + return nil + end + local want, got, disabled = 0, 0, false + for i = 1, table.getn(A.CHECK_NAMES) do + local folder = A.CHECK_NAMES[i] + if CHECK_SLOT[folder] == slot then + want = want + 1 + local r = byName[folder] + if r then + got = got + 1 + if r.s == "L" then + local e = entry(r.ver or "yes", nil, "addoncheck " .. folder .. "=L") + if not r.ver then + e.presenceOnly = true + end + e.at = r.at + e.responder = true + return { present = e } + elseif r.s == "D" then + disabled = true + end + end + end + end + if want == 0 or got < want then + return nil + end + return { missing = disabled and "installed but disabled (shootyepgp addon check)" + or "not installed (shootyepgp addon check)" } +end + -- drop everybody not in the given name set (members who left) function A.Prune(nameSet) for name in pairs(A.data) do @@ -321,11 +519,39 @@ function A.Prune(nameSet) A.dataCount = A.dataCount - 1 end end + for name in pairs(A.resp) do + if not nameSet[name] then + A.resp[name] = nil + A.respCount = A.respCount - 1 + end + end +end + +-- Remember when each roster name first appeared; forget names that left. +-- names = array of member names. +function A.NoteRoster(names, now) + local set = {} + for i = 1, table.getn(names) do + local nm = names[i] + set[nm] = true + if not A.since[nm] then + A.since[nm] = now + end + end + for nm in pairs(A.since) do + if not set[nm] then + A.since[nm] = nil + end + end end function A.Reset() A.data = {} A.dataCount = 0 + A.since = {} + A.resp = {} + A.respCount = 0 + A.checkId = nil A.query = { at = nil, sent = {}, roster = {} } A.lastStatus = nil end @@ -363,6 +589,8 @@ function A.StartQuery(env, auto) sent.TWT = true pcall(send, "Rinse", "REPORT_ADDON_VERSION", chan) sent.RINSE = true + -- one responder query on the same group channel (shootyepgp answers) + pcall(send, A.CHECK_PREFIX, A.NewCheckQuery(env.now), chan) if env.guild then pcall(send, "SWOW_Q", "1", "GUILD") sent.SUPERWOW = true @@ -450,6 +678,20 @@ function A.LocalEntries(g) else out.RINSE = false end + -- RABuffs / BetterCharacterStats: only the addon list is known locally + local function localPassive(addon) + if not g.loaded(addon) then + return false + end + local ver = g.meta(addon, "Version") + local e = mk(ver or "yes") + if not ver then + e.presenceOnly = true + end + return e + end + out.RABUFFS = localPassive("RABuffs") + out.BCS = localPassive("BetterCharacterStats") return out end @@ -527,6 +769,15 @@ end local NA_CLASS = "this class cannot dispel, so Rinse is not expected" local NA_GUILD = "not in your guild - shootyepgp only talks on guild chat" local NA_NOGLD = "you are not in a guild - this check needs guild chat" +local CONFIRM = " Only confirmed when the player runs shootyepgp with the" + .. " addon check." +local WHY_RAB = "No RABuffs message seen. Weak evidence only: RABuffs" + .. " broadcasts only when the member has mana or a tracked timed buff," + .. " so silence does not prove it is missing." .. CONFIRM +local WHY_RAB_WAIT = "no RABuffs message yet - RABuffs broadcasts every 30 s," + .. " so this needs about a minute in the raid." .. CONFIRM +local WHY_BCS = "No BetterCharacterStats message seen. It rarely sends, so" + .. " silence proves nothing." .. CONFIRM -- Annotate(players, ctx): sets p.addons[slot] and p.addonsAnswered on every -- player table. ctx: @@ -555,21 +806,32 @@ function A.Annotate(players, ctx) for s = 1, table.getn(A.SLOT_IDS) do local slot = A.SLOT_IDS[s] local r + local ownE = isOwn and ctx.ownEntries[slot] or nil + local resp = (not isOwn and ctx.responder) and ctx.responder(p, slot) or nil if p.online == false then r = { kind = "unknown", why = "offline" } + elseif ownE then + -- own character: the local addon list is certain, so an + -- installed addon shows even where the class rule below + -- would call it not expected + r = { kind = "present", e = ownE } + if not ownE.presenceOnly then + table.insert(lists[slot], ownE) + end + elseif resp and resp.present then + -- shootyepgp answered: certain, beats every passive rule + r = { kind = "present", e = resp.present } + if not resp.present.presenceOnly then + table.insert(lists[slot], resp.present) + end + elseif resp and resp.missing + and not (slot == "RINSE" and not A.DISPELLERS[p.class or ""]) then + r = { kind = "missing", why = resp.missing } elseif slot == "RINSE" and not A.DISPELLERS[p.class or ""] then r = { kind = "na", why = NA_CLASS } elseif isOwn then - local e = ctx.ownEntries[slot] - if e then - r = { kind = "present", e = e } - if not e.presenceOnly then - table.insert(lists[slot], e) - end - else - r = { kind = "missing", - why = "not installed on your own character" } - end + r = { kind = "missing", + why = "not installed on your own character" } elseif (slot == "EPGP" or slot == "SUPERWOW") and not ctx.inGuild(p) then r = { kind = "na", why = ctx.myGuild and NA_GUILD or NA_NOGLD } @@ -580,6 +842,17 @@ function A.Annotate(players, ctx) if not e.presenceOnly then table.insert(lists[slot], e) end + elseif slot == "RABUFFS" then + -- passive only: silence is weak evidence, shown as + -- "not seen" and only after two broadcast cycles + local secs = ctx.inGroupFor and ctx.inGroupFor(p) + if secs and secs >= A.RAB_SILENT then + r = { kind = "notseen", why = WHY_RAB } + else + r = { kind = "unknown", why = WHY_RAB_WAIT } + end + elseif slot == "BCS" then + r = { kind = "unknown", why = WHY_BCS } elseif ctx.silent(p, slot) then r = { kind = "missing" } else @@ -654,6 +927,8 @@ function A.Annotate(players, ctx) .. " that does not answer" } elseif r.kind == "na" then res = { status = "na", why = r.why } + elseif r.kind == "notseen" then + res = { status = "notseen", why = r.why } else res = { status = "unknown", why = r.why } end @@ -685,6 +960,13 @@ function A.RealCtx(env) silent = function(p, slot) return A.IsSilent(p.name, slot, now) end, + responder = function(p, slot) + return A.ResponderState(p.name, slot) + end, + inGroupFor = function(p) + local since = A.since[p.name] + return since and (now - since) or nil + end, } end @@ -712,6 +994,13 @@ function A.SimCtx(now) silent = function(p, slot) return p.addonSim ~= nil and p.addonSim[slot] == false end, + inGroupFor = function(p) + -- sim: a RABUFFS = false member has been around long enough + if p.addonSim ~= nil and p.addonSim.RABUFFS == false then + return A.RAB_SILENT + end + return nil + end, } end @@ -724,6 +1013,9 @@ function A.AnnotateStore(store, env) A.Annotate(store.players, A.SimCtx(env and env.now or 0)) return end + if env and env.roster and env.now then + A.NoteRoster(env.roster, env.now) + end A.Annotate(store.players, A.RealCtx(env)) -- evict against the live roster (not the store: before the first scan -- finishes the store is empty and would wipe everything) @@ -867,7 +1159,14 @@ end -- when replies came in and again when the query window ends local listener = CreateFrame("Frame", "DopingControlAddonsEventFrame", UIParent) listener:RegisterEvent("CHAT_MSG_ADDON") +listener:RegisterEvent("RAID_ROSTER_UPDATE") +listener:RegisterEvent("PARTY_MEMBERS_CHANGED") +listener:RegisterEvent("PLAYER_ENTERING_WORLD") listener:SetScript("OnEvent", function() + if event ~= "CHAT_MSG_ADDON" then + A.NoteRoster(names(), GetTime()) + return + end if A.OnAddonMessage(arg1, arg2, arg3, arg4, GetTime()) then A.dirty = true A.Wake() diff --git a/ui/matrix.lua b/ui/matrix.lua index 9e08be1..ceb82c9 100644 --- a/ui/matrix.lua +++ b/ui/matrix.lua @@ -965,6 +965,9 @@ function MX.AddonTooltipLines(sd, cvm, now) "fehlt" }) table.insert(lines, { a.why or "no reply - addon missing, or an old" .. " version / fork that does not answer", "parchDim", 1 }) + elseif st == "notseen" then + table.insert(lines, { "Not seen", "unbek" }) + table.insert(lines, { a.why or "no message seen", "parchDim", 1 }) elseif st == "na" then table.insert(lines, { "n/a", "parchDim" }) table.insert(lines, { a.why or "not applicable", "parchDim", 1 })