Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
755194d1cb |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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").
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+310
-11
@@ -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]
|
r = { kind = "missing",
|
||||||
if e then
|
why = "not installed on your own character" }
|
||||||
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
|
|
||||||
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()
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
Reference in New Issue
Block a user