DopingControl 0.8.1

This commit is contained in:
2026-10-18 13:41:37 +02:00
parent e6561d3bce
commit 755194d1cb
8 changed files with 444 additions and 23 deletions
+48
View File
@@ -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:<id>:<folders>`
for BigWigs, TWThreat, shootyepgp, UpsiRinse, Rinse, RABuffs and
BetterCharacterStats. shootyepgp builds with the responder answer
`A1:<id>:<Folder>=L/<version>;<Folder>=D;<Folder>=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
+1 -1
View File
@@ -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
+45 -6
View File
@@ -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:<id>:<Folder>,<Folder>,...` with at most 16 folder names. Answer:
`A1:<id>:<Folder>=L/<version>;<Folder>=D;<Folder>=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
+23 -4
View File
@@ -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." },
}
-- ------------------------------------------------------------------
+2 -1
View File
@@ -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").
+12
View File
@@ -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
+310 -11
View File
@@ -17,6 +17,25 @@
-- proves shootyepgp itself is installed.
-- Rinse prefix "Rinse": query "REPORT_ADDON_VERSION", reply
-- "V_<ver>" (only the UpsiRinse fork answers).
-- RABuffs passive only. Every client in a raid broadcasts on prefix
-- "RAB/BT" (RAID) every 30 s: "<key> <number>" pairs ("h2oc 20
-- fort 1500 ..."), but only when it has something to report
-- (mana users, tracked timed buffs). The version messages
-- "RAB/V" ("<ver> <deci>") 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,<name>[,<bonus>]", 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:<id>:<folders>" on
-- the group channel, every guild member's shootyepgp answers
-- "A1:<id>:<Folder>=L/<ver>;<Folder>=D;<Folder>=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 "<ver> <deci>" (pattern RABuffs itself uses)
-- gives the version; a "RAB/BT" payload is "<key> <number>" 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,<name>[,<bonus>]" 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:<id>:<Folder>=L/<ver>;<Folder>=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()
+3
View File
@@ -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 })