Files
DopingControl/scan/pull.lua
T
2026-10-15 12:29:40 +02:00

213 lines
9.5 KiB
Lua

-- DopingControl scan/pull.lua
-- Silent snapshot on a pull timer: BigWigs' Pulltimer plugin broadcasts a
-- CHAT_MSG_ADDON every time ANYONE in the raid/party starts (or rebroadcasts)
-- a pull timer -- one more moment where "who has their flask up" matters,
-- and one the addon currently misses entirely (only READY_CHECK triggers an
-- automatic snapshot -- scan/engine.lua's header -- and some raids' ready
-- checks route through a DIFFERENT addon's own protocol instead: prefix
-- "TW_RF", payload "<Name> Ready", never raising the native READY_CHECK
-- event at all (seen in a live capture). A whole Molten
-- Core night went unsnapshotted this way on 24 Sep 2026.
--
-- Wire format (source-confirmed against the installed BigWigs addon):
-- BigWigsComm:BigWigs_SendSync sends
-- SendAddonMessage("BigWigs", "<syncName> <rest>", "RAID")
-- (Plugins/Comm.lua:57-67) and receives the identical shape back on
-- CHAT_MSG_ADDON with prefix == "BigWigs" (Plugins/Comm.lua:43-54:
-- local _, _, sync, rest = string.find(message, "(%S+)%s*(.*)$")
-- -- the exact split used below). The pull-timer sync names
-- (Plugins/Pulltimer.lua:27-32):
-- "PulltimerSync" -- a (possibly soft/relayed) timer start,
-- rest = duration in seconds
-- "PulltimerBroadcastSync" -- an explicit rebroadcast, same rest shape
-- "PulltimerStopSync" -- pull aborted -- no duration, never a start
-- "PulltimerReqSync" -- "is a timer running" -- no duration
-- DoPull only ever Syncs a POSITIVE duration (Plugins/Pulltimer.lua:203-
-- 223: an explicit 0 either stops an existing bar or resolves to 6 before
-- sending), and BigWigs_Pulltimer itself discards anything `tonumber`
-- rejects (Plugins/Pulltimer.lua:256-260) -- so "sync name matches AND
-- rest is a positive number" is both necessary and sufficient for "a pull
-- just started".
-- No pfUI pull timer exists on this client (checked: no "pull" or
-- "Pulltimer" hit anywhere under Interface/AddOns/pfUI) -- BigWigs is the
-- only pull timer installed, so it is the only wire this file speaks.
-- Live confirmation the "BigWigs"/RAID channel is real traffic on this
-- server, not merely installed-but-idle code: captured chat addon
-- messages carry the same "<sync> <rest>" shape (a ward-fired notice).
-- No PulltimerSync sample was captured, so the exact in-game duration
-- argument is confirmed from source, not from a captured packet.
--
-- Self-pull: does the SENDER's own client receive its own CHAT_MSG_ADDON on
-- this fork? Unverified in-game either way -- but BigWigs' own code does not
-- trust it: BigWigs_SendSync calls SendAddonMessage AND ALSO invokes its own
-- CHAT_MSG_ADDON handler directly, passing the sender's own name, right
-- after (Plugins/Comm.lua:64-65). That is strong circumstantial evidence the
-- authors did not want a raid leader's own /pull to depend on the echo. This
-- file does the analogous thing: it hooks the global SendAddonMessage
-- (guarded, pcall-wrapped, every return value passed through unchanged) and
-- feeds prefix=="BigWigs" traffic through the SAME parser the CHAT_MSG_ADDON
-- handler uses. If the echo DOES fire on this client, the debounce below
-- absorbs the harmless double trigger; if it does not, the hook is the only
-- path a self-started pull has.
--
-- The scan itself rides the SAME entry point as the ready-check path
-- (DC_Scan.Start, scan/engine.lua) and is silent BY CONSTRUCTION: E.Start
-- never shows any UI frame -- the ready-check path's own auto-OPEN is a
-- SEPARATE decision made by core/init.lua's READY_CHECK handler
-- (db.readyCheckOpen -> DC_Matrix.Show()), which this file never touches or
-- even requires loaded. The resulting store gets a `trigger = "pull"` tag
-- (E.NewStore, scan/engine.lua) purely so dev/raiddump.lua's S line and the
-- store itself say which scans came from a pull timer; store.source stays
-- "scan" either way -- every real gate keeps reading THAT field.
--
-- Pure Lua 5.0, dofile-loadable offline: P.ParsePull/P.ShouldTrigger are
-- pure; the frame, the event and the SendAddonMessage hook sit behind
-- `if CreateFrame then`, the same convention as every other scan/*.lua file.
DopingControl = DopingControl or {}
local DC = DopingControl
DC_Pull = DC_Pull or {}
local P = DC_Pull
-- at most one pull-triggered scan every this many seconds
P.DEBOUNCE = 15
-- ==================================================================
-- Pure part
-- ==================================================================
-- ParsePull(prefix, msg) -> duration (number > 0) | nil
-- Recognizes a pull-timer START message on the BigWigs wire (see header).
-- Everything else (wrong prefix, a stop/req sync, a non-numeric or
-- non-positive rest) returns nil -- including a message from some OTHER
-- addon that happens to share the "BigWigs" prefix by accident, which would
-- simply fail the sync-name check.
function P.ParsePull(prefix, msg)
if prefix ~= "BigWigs" or type(msg) ~= "string" then
return nil
end
local _, _, sync, rest = string.find(msg, "(%S+)%s*(.*)$")
if sync ~= "PulltimerSync" and sync ~= "PulltimerBroadcastSync" then
return nil
end
local dur = tonumber(rest)
if dur and dur > 0 then
return dur
end
return nil
end
-- ShouldTrigger(now, lastAt, opts) -> bool
-- opts = { enabled, inGroup, simOwnsStore, scanRunning }
-- Every gate named in the brief, in one pure, orthogonal predicate: the
-- setting, group membership, the sim-store lock and the debounce window.
-- `scanRunning` is a SEPARATE gate from the debounce window -- a long scan
-- (a large raid, batched 5 units/frame in scan/engine.lua) can still be
-- running well past 15s, and a second pull during it must not restart it:
-- unlike a manual/ready-check Start(true), a pull-triggered Start is never
-- `force`, so DC_Scan.Start would already no-op on its own -- this gate just
-- avoids bumping `lastAt` (and thus the debounce window) for a call that was
-- never going to do anything.
function P.ShouldTrigger(now, lastAt, opts)
opts = opts or {}
if not opts.enabled then
return false
end
if opts.simOwnsStore then
return false
end
if not opts.inGroup then
return false
end
if opts.scanRunning then
return false
end
if lastAt and (now - lastAt) < P.DEBOUNCE then
return false
end
return true
end
-- ==================================================================
-- WoW part
-- ==================================================================
if CreateFrame then
local lastAt = nil
-- Raid count first, party count only when it is 0 -- same convention as
-- core/init.lua's (private) GroupSize(): both read the same two API
-- calls and would drift identically if either ever changed shape.
local function inGroup()
local n = GetNumRaidMembers and GetNumRaidMembers() or 0
if n and n > 0 then
return true
end
n = GetNumPartyMembers and GetNumPartyMembers() or 0
return (n or 0) > 0
end
local function pullEnabled(db)
-- default ON, like every other auto-scan switch in this file family
-- (db.readyCheckScan, core/config.lua) -- NEVER `db.pullTimerScan`
-- alone (would misread a saved `false` as truthy-missing).
return not (db and db.pullTimerScan == false)
end
-- Shared by the CHAT_MSG_ADDON handler and the SendAddonMessage hook
-- below -- one decision path, fed from two entry points (see header).
local function maybeTrigger(prefix, msg)
local dur = P.ParsePull(prefix, msg)
if not dur then
return
end
local db = DopingControlDB
local now = GetTime and GetTime() or 0
local scanRunning = (DC_Scan and DC_Scan.IsRunning
and DC_Scan.IsRunning()) or false
local go = P.ShouldTrigger(now, lastAt, {
enabled = pullEnabled(db),
inGroup = inGroup(),
simOwnsStore = DC.SimOwnsStore(db),
scanRunning = scanRunning,
})
if not go then
return
end
lastAt = now
if DC_Scan and DC_Scan.Start then
DC_Scan.Start(nil, "pull")
end
end
local f = CreateFrame("Frame", "DopingControlPullFrame")
f:RegisterEvent("CHAT_MSG_ADDON")
f:SetScript("OnEvent", function()
-- 1.12: arg1 prefix, arg2 message, arg3 channel, arg4 sender -- the
-- channel/sender are not needed here, unlike scan/talents.lua: ANY
-- raid/party member's pull (including our own, see below) is a
-- trigger, so there is nothing left to filter on.
maybeTrigger(arg1, arg2)
end)
-- Self-pull backstop -- see the file header. Installed once; pcall-
-- wrapped so a malformed call from ANY addon can never take the real
-- SendAddonMessage down with it, and every return value is passed
-- through unchanged so no other addon (BigWigs included) can tell it
-- was wrapped.
if SendAddonMessage and not P.hooked then
P.hooked = true
local orig = SendAddonMessage
P.origSendAddonMessage = orig -- exposed for tests
SendAddonMessage = function(prefix, msg, chatType, target)
-- our own logic is guarded so it can never break the caller's
-- send; the original runs unguarded so its errors still reach
-- the calling addon exactly as before the hook
pcall(maybeTrigger, prefix, msg)
return orig(prefix, msg, chatType, target)
end
end
end