1 Commits
Author SHA1 Message Date
ShempError 755194d1cb DopingControl 0.8.1 2026-10-18 13:41:37 +02:00
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 released here — a new feature raises the middle number and resets the last
one, the last number is for fixes without new features. 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 ## v0.7.0
### New Addons tab: who in the raid runs BigWigs, Threat Meter, EPGP, SuperWoW, Rinse ### New Addons tab: who in the raid runs BigWigs, Threat Meter, EPGP, SuperWoW, Rinse
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: DopingControl ## Title: DopingControl
## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker ## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker
## Author: ShempError ## Author: ShempError
## Version: 0.7.0 ## Version: 0.8.1
## SavedVariables: DopingControlDB ## SavedVariables: DopingControlDB
core\const.lua 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 in the cells, ready pills per role group, gaps first, unreadable players marked instead of
blamed.* 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 ## What's new — v0.7.0 · 2026-10-15
- **A new Addons tab** shows which raid addons the other raiders run — BigWigs, Threat Meter, - **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): **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); - **v0.7.0** — the **Addons tab** (BigWigs, Threat Meter, EPGP, SuperWoW, Rinse across the raid);
the Equipment tooltip names the enchant. the Equipment tooltip names the enchant.
- **v0.6.4** — the equipment expectation top-up self-heals on every refresh, so a profile - **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.* addon is not expected.*
The question this tab answers is **"who in my raid runs the addons I rely on?"** — BigWigs, 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. 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 **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 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 "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 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 **Check addons** button sends one burst of queries and collects replies for six seconds.
The status line next to it counts the window down. 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, 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. | | 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. | | 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. | | 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. | | 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 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 - **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. 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 - **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 else shows a dot unless their addon check reports Rinse loaded. Only the **UpsiRinse** fork
Rinse shows red even though the addon is installed. 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 **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 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" limits = "Guild members running shootyepgp only. The SuperWoW DLL"
.. " itself cannot be detected." }, .. " itself cannot be detected." },
{ id = "RINSE", label = "RIN", sub = "dispel", full = "Rinse", kind = "addon", { id = "RINSE", label = "RIN", sub = "dispel", full = "Rinse", kind = "addon",
how = "Asks the group with the UpsiRinse version query; it answers" how = "Asks the group with the UpsiRinse version query and the"
.. " with its version.", .. " 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" limits = "Only dispellers (Druid, Priest, Paladin, Shaman, Mage) are"
.. " checked, and only the UpsiRinse fork answers - the original" .. " checked for a miss. Without a shootyepgp answer only the"
.. " Rinse does not." }, .. " 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 if kind == "addon" then
-- ADDONS tab: the cell is the evaluated reply scan/addons.lua put -- ADDONS tab: the cell is the evaluated reply scan/addons.lua put
-- on player.addons[slot] ({ status = ok | old | missing | na | -- 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 -- 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 -- 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"). -- when the addon is known absent (own row, SuperWoW answer "0").
+12
View File
@@ -534,6 +534,18 @@ local function simAddons(i)
else else
m.RINSE = { text = "3.0" } m.RINSE = { text = "3.0" }
end 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 return m
end end
+308 -9
View File
@@ -17,6 +17,25 @@
-- proves shootyepgp itself is installed. -- proves shootyepgp itself is installed.
-- Rinse prefix "Rinse": query "REPORT_ADDON_VERSION", reply -- Rinse prefix "Rinse": query "REPORT_ADDON_VERSION", reply
-- "V_<ver>" (only the UpsiRinse fork answers). -- "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 -- 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; -- 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 -- 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.WINDOW = 6 -- seconds a query waits for replies
A.AUTO_THROTTLE = 30 -- seconds between automatic queries A.AUTO_THROTTLE = 30 -- seconds between automatic queries
A.MAX_NAMES = 400 -- passive-store cap (guild chatter from non-raiders) 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) -- 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 = { A.DISPELLERS = {
DRUID = true, PRIEST = true, PALADIN = true, SHAMAN = true, MAGE = true, DRUID = true, PRIEST = true, PALADIN = true, SHAMAN = true, MAGE = true,
@@ -48,13 +69,41 @@ local PREFIX_SLOT = {
["Rinse"] = "RINSE", ["Rinse"] = "RINSE",
["SEPGP_PREFIX"] = "EPGP", ["SEPGP_PREFIX"] = "EPGP",
["SWOW_R"] = "SUPERWOW", ["SWOW_R"] = "SUPERWOW",
["RAB/BT"] = "RABUFFS",
["RAB/V"] = "RABUFFS",
["RAB/VC"] = "RABUFFS",
["bcs"] = "BCS",
["ADDONCHECK"] = "CHECK", -- responder answers, own parser
} }
A.PREFIXES = PREFIX_SLOT 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 -- passive store: A.data[name][slot] = entry
A.data = {} A.data = {}
A.dataCount = 0 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 -- query state
A.query = { at = nil, sent = {}, roster = {} } A.query = { at = nil, sent = {}, roster = {} }
A.lastStatus = nil -- last StartQuery verdict text (shown next to the button) A.lastStatus = nil -- last StartQuery verdict text (shown next to the button)
@@ -252,12 +301,49 @@ function A.ParseSwow(msg)
return nil return nil
end 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 = { local PARSER = {
BIGWIGS = A.ParseBigWigs, BIGWIGS = A.ParseBigWigs,
TWT = A.ParseTWT, TWT = A.ParseTWT,
RINSE = A.ParseRinse, RINSE = A.ParseRinse,
EPGP = A.ParseEPGP, EPGP = A.ParseEPGP,
SUPERWOW = A.ParseSwow, 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 if not slot or type(sender) ~= "string" or sender == A.own then
return false return false
end end
if slot == "CHECK" then
return A.OnCheckAnswer(msg, channel, sender, now)
end
-- BigWigs itself only honours its syncs on RAID -- BigWigs itself only honours its syncs on RAID
if slot == "BIGWIGS" and channel ~= "RAID" then if slot == "BIGWIGS" and channel ~= "RAID" then
return false return false
@@ -313,6 +402,115 @@ function A.OnAddonMessage(prefix, msg, channel, sender, now)
return changed return changed
end 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) -- drop everybody not in the given name set (members who left)
function A.Prune(nameSet) function A.Prune(nameSet)
for name in pairs(A.data) do for name in pairs(A.data) do
@@ -321,11 +519,39 @@ function A.Prune(nameSet)
A.dataCount = A.dataCount - 1 A.dataCount = A.dataCount - 1
end end
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 end
function A.Reset() function A.Reset()
A.data = {} A.data = {}
A.dataCount = 0 A.dataCount = 0
A.since = {}
A.resp = {}
A.respCount = 0
A.checkId = nil
A.query = { at = nil, sent = {}, roster = {} } A.query = { at = nil, sent = {}, roster = {} }
A.lastStatus = nil A.lastStatus = nil
end end
@@ -363,6 +589,8 @@ function A.StartQuery(env, auto)
sent.TWT = true sent.TWT = true
pcall(send, "Rinse", "REPORT_ADDON_VERSION", chan) pcall(send, "Rinse", "REPORT_ADDON_VERSION", chan)
sent.RINSE = true 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 if env.guild then
pcall(send, "SWOW_Q", "1", "GUILD") pcall(send, "SWOW_Q", "1", "GUILD")
sent.SUPERWOW = true sent.SUPERWOW = true
@@ -450,6 +678,20 @@ function A.LocalEntries(g)
else else
out.RINSE = false out.RINSE = false
end 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 return out
end end
@@ -527,6 +769,15 @@ end
local NA_CLASS = "this class cannot dispel, so Rinse is not expected" 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_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 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 -- Annotate(players, ctx): sets p.addons[slot] and p.addonsAnswered on every
-- player table. ctx: -- player table. ctx:
@@ -555,21 +806,32 @@ function A.Annotate(players, ctx)
for s = 1, table.getn(A.SLOT_IDS) do for s = 1, table.getn(A.SLOT_IDS) do
local slot = A.SLOT_IDS[s] local slot = A.SLOT_IDS[s]
local r 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 if p.online == false then
r = { kind = "unknown", why = "offline" } 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 elseif slot == "RINSE" and not A.DISPELLERS[p.class or ""] then
r = { kind = "na", why = NA_CLASS } r = { kind = "na", why = NA_CLASS }
elseif isOwn then 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", r = { kind = "missing",
why = "not installed on your own character" } why = "not installed on your own character" }
end
elseif (slot == "EPGP" or slot == "SUPERWOW") elseif (slot == "EPGP" or slot == "SUPERWOW")
and not ctx.inGuild(p) then and not ctx.inGuild(p) then
r = { kind = "na", why = ctx.myGuild and NA_GUILD or NA_NOGLD } 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 if not e.presenceOnly then
table.insert(lists[slot], e) table.insert(lists[slot], e)
end 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 elseif ctx.silent(p, slot) then
r = { kind = "missing" } r = { kind = "missing" }
else else
@@ -654,6 +927,8 @@ function A.Annotate(players, ctx)
.. " that does not answer" } .. " that does not answer" }
elseif r.kind == "na" then elseif r.kind == "na" then
res = { status = "na", why = r.why } res = { status = "na", why = r.why }
elseif r.kind == "notseen" then
res = { status = "notseen", why = r.why }
else else
res = { status = "unknown", why = r.why } res = { status = "unknown", why = r.why }
end end
@@ -685,6 +960,13 @@ function A.RealCtx(env)
silent = function(p, slot) silent = function(p, slot)
return A.IsSilent(p.name, slot, now) return A.IsSilent(p.name, slot, now)
end, 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 end
@@ -712,6 +994,13 @@ function A.SimCtx(now)
silent = function(p, slot) silent = function(p, slot)
return p.addonSim ~= nil and p.addonSim[slot] == false return p.addonSim ~= nil and p.addonSim[slot] == false
end, 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 end
@@ -724,6 +1013,9 @@ function A.AnnotateStore(store, env)
A.Annotate(store.players, A.SimCtx(env and env.now or 0)) A.Annotate(store.players, A.SimCtx(env and env.now or 0))
return return
end end
if env and env.roster and env.now then
A.NoteRoster(env.roster, env.now)
end
A.Annotate(store.players, A.RealCtx(env)) A.Annotate(store.players, A.RealCtx(env))
-- evict against the live roster (not the store: before the first scan -- evict against the live roster (not the store: before the first scan
-- finishes the store is empty and would wipe everything) -- 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 -- when replies came in and again when the query window ends
local listener = CreateFrame("Frame", "DopingControlAddonsEventFrame", UIParent) local listener = CreateFrame("Frame", "DopingControlAddonsEventFrame", UIParent)
listener:RegisterEvent("CHAT_MSG_ADDON") listener:RegisterEvent("CHAT_MSG_ADDON")
listener:RegisterEvent("RAID_ROSTER_UPDATE")
listener:RegisterEvent("PARTY_MEMBERS_CHANGED")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
listener:SetScript("OnEvent", function() 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 if A.OnAddonMessage(arg1, arg2, arg3, arg4, GetTime()) then
A.dirty = true A.dirty = true
A.Wake() A.Wake()
+3
View File
@@ -965,6 +965,9 @@ function MX.AddonTooltipLines(sd, cvm, now)
"fehlt" }) "fehlt" })
table.insert(lines, { a.why or "no reply - addon missing, or an old" table.insert(lines, { a.why or "no reply - addon missing, or an old"
.. " version / fork that does not answer", "parchDim", 1 }) .. " 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 elseif st == "na" then
table.insert(lines, { "n/a", "parchDim" }) table.insert(lines, { "n/a", "parchDim" })
table.insert(lines, { a.why or "not applicable", "parchDim", 1 }) table.insert(lines, { a.why or "not applicable", "parchDim", 1 })