-- TotemBar - core/pulsecal.lua -- Dev-only pulse-calibration telemetry (/tb pulsecal): captures raw -- totem-related combat lines + our own placements into a fixed ring buffer -- and exports them via SuperWoW ExportFile, so the dev environment can -- measure REAL TurtleWoW pulse intervals offline and bake them back into -- core/pulsedata.lua (verified=true). -- Pure buffer/serializer on top (offline-tested); WoW-gated capture below. TotemBar = TotemBar or {} TotemBar.PULSECAL_CAP = 2000 -- Pure: push one record into the ring buffer. state = { n = total pushed, -- idx = next write slot 1..cap }. Record tables are REUSED on wrap (no -- allocation growth while capturing). function TotemBar.pulsecalPush(buf, cap, state, t, ev, msg) local slot = state.idx local rec = buf[slot] if not rec then rec = {} buf[slot] = rec end rec.t = t rec.e = ev rec.m = msg state.n = state.n + 1 state.idx = slot + 1 if state.idx > cap then state.idx = 1 end end -- Pure: serialize surviving records in chronological order, one -- "t;event;msg" line each (t with millisecond precision). Allocates - dump -- time only, never per capture. function TotemBar.pulsecalFormat(buf, cap, state) local count = state.n if count > cap then count = cap end if count == 0 then return "" end local start = state.idx - count if start < 1 then start = start + cap end local lines = {} for i = 0, count - 1 do local slot = start + i if slot > cap then slot = slot - cap end local rec = buf[slot] lines[i + 1] = string.format("%.3f;%s;%s", rec.t, rec.e, rec.m) end return table.concat(lines, "\n") end -- --------------------------------------------------------------------------- -- WoW-gated capture (skipped entirely by the offline test runner). if CreateFrame then local ChatOut = DEFAULT_CHAT_FRAME or ChatFrame1 local capturing = false local buf = {} local state = { n = 0, idx = 1 } -- Everything that could plausibly carry a totem pulse on 1.12 (no -- COMBAT_LOG_EVENT_UNFILTERED here - all localized text in arg1). local CAPTURE_EVENTS = { "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_SELF_DAMAGE", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE", "CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS", "CHAT_MSG_SPELL_AURA_GONE_SELF", "CHAT_MSG_SPELL_AURA_GONE_PARTY", } local calFrame = CreateFrame("Frame", "TotemBarPulseCalFrame", UIParent) calFrame:SetScript("OnEvent", function() if not capturing then return end if event == "UNIT_CASTEVENT" then -- SuperWoW: casterGUID, targetGUID, type, spellId, castTime. -- Only record casts whose spell name mentions Totem (SpellInfo -- is a SuperWoW API; guarded because the event only exists there -- anyway). local sname = SpellInfo and SpellInfo(arg4) if sname and string.find(sname, "Totem", 1, true) then TotemBar.pulsecalPush(buf, TotemBar.PULSECAL_CAP, state, GetTime(), event, tostring(arg1) .. ";" .. tostring(arg3) .. ";" .. tostring(sname)) end return end -- Plain substring guard keeps the hot path cheap; "Totem"-less buff -- names (Mana Spring / Healing Stream gain lines) are the exception, -- so include the "You gain" prefix too. local msg = arg1 if msg and (string.find(msg, "Totem", 1, true) or string.find(msg, "You gain", 1, true)) then TotemBar.pulsecalPush(buf, TotemBar.PULSECAL_CAP, state, GetTime(), event, msg) end end) -- Record our own placements as reference marks (t0 for interval math). -- Wrapping recordCast is safe here: core/pulsecal.lua loads AFTER -- core/cast.lua (see .toc) and ui.lua resolves TotemBar.recordCast at -- call time. local origRecordCast = TotemBar.recordCast TotemBar.recordCast = function(element, totemName) origRecordCast(element, totemName) if capturing then TotemBar.pulsecalPush(buf, TotemBar.PULSECAL_CAP, state, GetTime(), "TB_PLACED", tostring(element) .. ";" .. tostring(totemName)) end end function TotemBar.PulseCal(sub) if sub == "start" then if not capturing then capturing = true for i = 1, table.getn(CAPTURE_EVENTS) do calFrame:RegisterEvent(CAPTURE_EVENTS[i]) end -- SuperWoW-only event; pcall-guarded in case a client build -- rejects unknown event names. pcall(function() calFrame:RegisterEvent("UNIT_CASTEVENT") end) end ChatOut:AddMessage("TotemBar: pulsecal capture STARTED (drop totems, then /tb pulsecal dump).") elseif sub == "stop" then capturing = false calFrame:UnregisterAllEvents() ChatOut:AddMessage("TotemBar: pulsecal capture stopped (" .. state.n .. " records kept).") elseif sub == "dump" then if not ExportFile then ChatOut:AddMessage("TotemBar: pulsecal dump needs SuperWoW (ExportFile missing).") return end -- Filename WITHOUT extension - the client appends .txt itself. ExportFile("totembar_pulsecal", TotemBar.pulsecalFormat(buf, TotemBar.PULSECAL_CAP, state)) ChatOut:AddMessage("TotemBar: pulsecal dump written (imports\\totembar_pulsecal.txt, " .. state.n .. " records).") else local on = capturing and "ON" or "OFF" ChatOut:AddMessage("TotemBar: pulsecal " .. on .. ", " .. state.n .. " records. Usage: /tb pulsecal start|stop|dump") end end end