DopingControl v0.6.4

This commit is contained in:
2026-08-30 15:37:17 +02:00
parent ecab5ba845
commit 695dd9dcd4
23 changed files with 2278 additions and 296 deletions
+62 -6
View File
@@ -67,6 +67,11 @@ T.ASK_GAP = 5
-- do not ask the same player again for this long. Talents change on
-- respec, which is rare and never mid-raid.
T.REASK_AFTER = 1800
-- how long an unfinished reply stays open. A whole reply lands within a
-- fraction of a second, so a line arriving this much later cannot belong to
-- it -- it is the start of a NEW reply and must not be folded into the old
-- accumulator (see T.AccStale).
T.ACC_TTL = 10
-- ==================================================================
-- PURE SECTION (offline-testable)
@@ -147,9 +152,24 @@ end
-- spent = <sum of pointsSpent over the trees that reported>,
-- sumRank = <sum of every rank we stored>,
-- trees = <how many tab lines arrived>,
-- complete = <INSTalentEND seen> }
function T.NewAcc()
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false }
-- complete = <INSTalentEND seen>,
-- at = <when the first line arrived, nil offline> }
function T.NewAcc(now)
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false,
at = now }
end
-- Has this accumulator been waiting so long that the next line must belong to
-- a different reply? Only the END marker drops an accumulator, so a reply
-- whose tail was lost would otherwise stay open forever and swallow the next
-- one: tab lines add up twice while ranks are deduplicated by name, so
-- T.Plausible rejects a perfectly good retry. Without a clock (offline) or an
-- opening stamp nothing expires -- the old behaviour, unchanged.
function T.AccStale(acc, now)
if not acc or not acc.at or not now then
return false
end
return (now - acc.at) > T.ACC_TTL
end
-- Fold one parsed line into the accumulator. Returns the accumulator so
@@ -281,6 +301,9 @@ if CreateFrame then
return ok
end
-- set below, once the frame exists: arms the paced sender
local arm
-- Called by the scan with the players it can see. Nothing is sent here
-- -- names are only lined up; the timer below paces them out.
function T.Request(names)
@@ -296,6 +319,9 @@ if CreateFrame then
table.insert(queue, n)
end
end
if arm and table.getn(queue) > 0 then
arm()
end
end
local f = CreateFrame("Frame", "DopingControlTalentFrame")
@@ -313,9 +339,14 @@ if CreateFrame then
if not kind then
return
end
local now = GetTime()
local a = acc[sender]
if a and T.AccStale(a, now) then
-- the previous reply lost its END line; this one starts fresh
a = nil
end
if not a then
a = T.NewAcc()
a = T.NewAcc(now)
acc[sender] = a
end
T.Feed(a, kind, data)
@@ -338,14 +369,26 @@ if CreateFrame then
-- queue the scan filled. Deliberately a plain timer rather than a burst
-- after each scan -- 40 requests in one frame is what a rate limit
-- would punish, and nothing documents where that limit sits.
--
-- It is installed only while there is something to send and takes itself
-- off again the moment the queue runs dry (or the feature is switched
-- off): an OnUpdate runs 60+ times a second for the whole session, and a
-- player who never scans must not pay for a queue that is always empty.
-- Re-arming needs no extra wiring -- every scan calls T.Request.
local elapsed = 0
f:SetScript("OnUpdate", function()
local function disarm()
f:SetScript("OnUpdate", nil)
end
local function drain()
elapsed = elapsed + (arg1 or 0)
if elapsed < 1 then
return
end
elapsed = 0
if not enabled() or table.getn(queue) == 0 then
disarm()
return
end
local now = GetTime()
@@ -359,10 +402,23 @@ if CreateFrame then
T.Ask(name)
end
end
end)
if table.getn(queue) == 0 then
disarm()
end
end
arm = function()
if f:GetScript("OnUpdate") then
return
end
-- a long idle gap must not fire an immediate burst
elapsed = 0
f:SetScript("OnUpdate", drain)
end
-- forget everything (respec, or a deliberate re-read)
function T.Clear()
disarm()
acc = {}
ranks = {}
readAt = {}