441 lines
24 KiB
Lua
441 lines
24 KiB
Lua
-- Vampify -- dev-only per-hit debug export. Pure Lua ring buffer + line formatting (offline
|
|
-- tested), plus a thin session-local singleton and SuperWoW ExportFile flush.
|
|
--
|
|
-- WHAT THIS IS FOR. The v0.1/v0.2 formula (heal(D) = max(nSources, D * P * a)) uses ONE factor
|
|
-- per source, summed. Upgrading that to a PER-SOURCE factor needs exact measurements of what the
|
|
-- model predicted against what actually happened for every hit -- not the aggregated totals
|
|
-- /vf status already prints. This channel is that instrument: one line per own hit with the
|
|
-- inputs the model saw, the prediction it made, and the health readings around it, dumped to
|
|
-- <WoW>\imports\ for offline analysis.
|
|
--
|
|
-- INC / SELFHEAL / EXTHEAL RUN INDEPENDENTLY OF /vf watch AND GROUP STATUS (follow-up change
|
|
-- request, 2026-08-22). /vf watch on correctly REFUSES in a group -- the HP-reconciliation
|
|
-- BALANCE it computes is genuinely meaningless there (foreign healing, measured at +29..49%
|
|
-- phantom healing). That refusal is about watch's own verdict, not about whether the raw incoming/
|
|
-- healing numbers can be captured at all -- and this channel wants exactly those raw numbers,
|
|
-- group or not, watch-on or not, since they are what turns "some healing arrived and we don't know
|
|
-- why" into a measurable EXTHEAL line. So capture/incoming.lua now tracks two independent "wants"
|
|
-- (I.setWatchWanted / I.setPerHitWanted); whichever hit-line code in core/commands.lua feeds
|
|
-- PH.incoming/.selfheal/.extheal runs whenever EITHER wants the capture (I.isOn()), while the
|
|
-- watch balance itself (VampifyWatch.addWindow) stays gated on I.isWatchOn() specifically -- see
|
|
-- capture/incoming.lua's own header for the full mechanism and for the answer to "does Vampify see
|
|
-- foreign healing at all" (yes: SPELL_HEAL_BY_OTHER, always already registered; it was just being
|
|
-- folded into the self-heal total before this change, which is what the new EXTHEAL line fixes).
|
|
--
|
|
-- DEFAULT ON, PERSISTED (follow-up change request, 2026-08-22, overriding this addon's earlier
|
|
-- "dev-only, default off, never saved" stance for THIS specific channel by explicit developer
|
|
-- request): it is meant to run continuously across every session like a background capture addon's
|
|
-- capture, not be armed by hand. The persisted half of that --
|
|
-- VampifyDB.perhitEnabled and the auto-enable-on-login sync -- lives in core/commands.lua
|
|
-- (syncPerHit) and gui/options.lua (the "Per-hit debug export" checkbox); this module itself still
|
|
-- defaults its OWN in-memory VampifyPerHit._state to disabled (PH.newState()) and knows nothing
|
|
-- about VampifyConfig -- it only turns on when told to, the same as every other module here that
|
|
-- stays config-agnostic (core/model.lua, core/watch.lua). /vf perhit on|off|status
|
|
-- (core/commands.lua) remains the manual alternative and writes the same persisted field.
|
|
--
|
|
-- ARCHITECTURE. Same split as the rest of this addon: this file is pure and WoW-API-free except
|
|
-- for the disk flush at the bottom (mirrors core/const.lua's error capture and core/watch.lua's
|
|
-- watchLine, which are the same shape for the same reason). The actual per-hit DATA -- amount,
|
|
-- isAoE, spellId, the model's healFloat, sumPercent/nSources -- lives in core/commands.lua's
|
|
-- onDamage listener, which is "the only place where the pure core meets the WoW-bound capture
|
|
-- layer" (its own header comment). So commands.lua calls VampifyPerHit.hit(...)/.incoming(...)/
|
|
-- .selfheal(...)/.extheal(...)/.onTick(...) with values it already has; this file registers NO
|
|
-- event listeners of its own and needs no new ones in capture/*.lua.
|
|
--
|
|
-- hp_max (follow-up change request, 2026-08-22). Overheal windows are otherwise invisible to offline analysis: at
|
|
-- full health the Vampirism heal lands and immediately overflows, so hp_after - hp_before reads 0
|
|
-- even though pred_heal > 0 -- indistinguishable, from the numbers alone, from the heal genuinely
|
|
-- not landing (a real model deviation). hp_max fixes that: with hp_before/hp_max/pred_heal an
|
|
-- offline reader can tell "delta=0 because hp_before was already at/near hp_max" apart from
|
|
-- "delta=0 and hp_before had headroom -- something is actually wrong". Read via
|
|
-- UnitHealthMax("player") at the SAME point as hp_before (when the hit is recorded, not when the
|
|
-- window closes) -- hp_max does not change mid-fight under anything this addon cares about, so
|
|
-- there is no "before/after" question for it the way there is for hp itself, and reading it
|
|
-- alongside hp_before costs nothing new (no event, one more already-cheap API call at a point that
|
|
-- already reads UnitHealth).
|
|
--
|
|
-- hp_before / hp_after. Vampirism heals with NO event of its own (that is the addon's entire
|
|
-- reason to exist), so the only way to see it land is to read UnitHealth("player") before and
|
|
-- after. "Before" is the health at the moment this hit's damage event fires (before the healing
|
|
-- from THIS hit can possibly have landed). "After" is NOT available at that same instant --
|
|
-- 1.12's event order gives no signal for "the heal from this hit has now applied" -- so it is
|
|
-- read at the EARLIEST of two things that already exist in commands.lua: the next own damage
|
|
-- event (closing this hit's row the same way the HP-reconciliation watchdog already closes its
|
|
-- windows, core/watch.lua's "Stage 2" comment), or the next OnUpdate throttle tick (~0.25s) if no
|
|
-- further hit arrives in time. A hit right before combat ends is force-closed on
|
|
-- PLAYER_REGEN_ENABLED/PLAYER_DEAD (PH.closeWindow) so nothing is left pending across a long idle
|
|
-- period -- see PH.closeWindow's own comment for why that does NOT also force a disk write.
|
|
-- CONSEQUENCE FOR ANALYSIS: hp_after is a snapshot up to ~0.3s after the hit, not an
|
|
-- exact "immediately after this heal" read -- it can include a second hit's healing (or unrelated
|
|
-- incoming damage) if events land close together. Treat hp_after - hp_before as "what happened in
|
|
-- the window this hit opened", not as an exact isolation of this one heal; the /vf watch balance
|
|
-- (core/watch.lua) exists for the same reason and has the same caveat.
|
|
--
|
|
-- WHAT IS NOT COVERED. The retroactive AoE correction (VampifyDamage.onAoECorrection,
|
|
-- commands.lua) rewrites an already-recorded hit's healing after the fact once a second target
|
|
-- proves it was AoE -- but by the time that fires, this channel's line for the original hit may
|
|
-- already be flushed to disk. Reopening a flushed line is out of scope (this is an append-only
|
|
-- export, like every other channel in this addon); a hit that gets corrected will show its
|
|
-- ORIGINAL (non-AoE) aoe=0/pred_heal in its own HIT line. Cross-reference against
|
|
-- vampify_watch.txt / the session's own knowledge of which spells proc AoE if this matters to a
|
|
-- specific analysis.
|
|
|
|
VampifyPerHit = {}
|
|
local PH = VampifyPerHit
|
|
|
|
-- ---- pure: buffer + formatting ------------------------------------------------------------------
|
|
|
|
-- Hard safety cap on the in-memory buffer, independent of the flush thresholds below. Normal
|
|
-- operation flushes long before this; it exists so a flush that somehow never fires (ExportFile
|
|
-- missing, disk full) degrades into "oldest lines drop" instead of unbounded growth -- the same
|
|
-- discipline as core/watch.lua's WATCH_LOG_MAX, sized larger here because a per-hit line is
|
|
-- shorter and AoE grinding can produce 30-60 hits/sec.
|
|
local RING_CAP = 2000
|
|
PH.RING_CAP = RING_CAP
|
|
|
|
-- A hit is force-closed (hp_after read "now") if no further own hit arrives within this long.
|
|
-- Set just above the 0.25s display throttle commands.lua already runs PH.onTick from, so an
|
|
-- isolated hit's hp_after is captured on the very next tick rather than staying open until
|
|
-- whatever hit happens to come next (which could be seconds later, or never, in that fight).
|
|
local PENDING_TIMEOUT = 0.3
|
|
PH.PENDING_TIMEOUT = PENDING_TIMEOUT
|
|
|
|
function PH.newState()
|
|
return {
|
|
enabled = false,
|
|
sid = nil,
|
|
chunk = 0,
|
|
buf = {},
|
|
pending = nil,
|
|
totalHits = 0,
|
|
totalFlushed = 0,
|
|
elapsed = 0,
|
|
}
|
|
end
|
|
|
|
-- Only acts on a TRANSITION. Turning on (re)seeds sid/chunk/buffer/counters -- a fresh instrument
|
|
-- read, not a continuation of whatever a previous /vf perhit on..off cycle left behind. Turning
|
|
-- off leaves sid/chunk/buf alone on purpose: the caller (PH.disable) still needs them to flush
|
|
-- the final chunk under the SAME sid/chunk index the session was using.
|
|
function PH.setEnabled(state, on, sid)
|
|
on = on and true or false
|
|
if on == state.enabled then return state.enabled end
|
|
if on then
|
|
state.sid, state.chunk = sid, 0
|
|
VampifyConst.resetList(state.buf) -- see resetList's own comment: nil-ing indices alone
|
|
-- leaves table.getn reading a stale n in Lua 5.0
|
|
state.pending = nil
|
|
state.totalHits, state.totalFlushed, state.elapsed = 0, 0, 0
|
|
end
|
|
state.enabled = on
|
|
return state.enabled
|
|
end
|
|
|
|
-- table.remove (not a hand-rolled shift) to evict the oldest line: it keeps `n` in step, the same
|
|
-- reasoning core/watch.lua's watchLine gives for using it over nil-ing indices by hand.
|
|
function PH.pushLine(state, line)
|
|
table.insert(state.buf, line)
|
|
if table.getn(state.buf) > RING_CAP then
|
|
table.remove(state.buf, 1)
|
|
end
|
|
end
|
|
|
|
-- The INDIVIDUAL source percentages of the hit, as "3/2/2/2".
|
|
--
|
|
-- WHY IT HAD TO BE ADDED (offline analysis, 2026-08-30). The line carried only the SUMMED
|
|
-- percentage P and the source count n, and the server truncates every source SEPARATELY
|
|
-- (core/model.lua's header): max(1, floor(pct_i * damage / 100)) per source. From P and n alone
|
|
-- that is not reconstructable -- an offline reader has to approximate the list as n equal shares,
|
|
-- which is exactly VampifyModel.heal's documented-inexact compatibility shape. Measured against
|
|
-- the recorded pred_heal over 4907 windows that approximation is off by 4.7 %, and 4.7 % is the
|
|
-- same order as the effects the analysis is trying to separate. With the real list the offline
|
|
-- model becomes exact instead of approximate, and the AoE-ordering question core/model.lua's
|
|
-- header leaves open (damp the damage, or damp each source's heal) becomes decidable from
|
|
-- recorded data rather than needing a new in-game measurement.
|
|
--
|
|
-- "%g", not "%d" or "%.2f": a source percentage is normally a small integer (3, 2, 2) and should
|
|
-- print as one, but nothing guarantees it -- a fractional percentage must survive rather than be
|
|
-- silently truncated into a wrong number.
|
|
--
|
|
-- Buffer-recycled like every other reused list here (VampifyConst.resetList's comment): this runs
|
|
-- once per own hit, and a fresh table per hit is avoidable churn on a channel that is meant to run
|
|
-- permanently. The caller's list is READ ONLY -- and the result is a STRING, which matters: the
|
|
-- live source list is itself a recycled buffer (core/commands.lua's sourcePercents, refilled in
|
|
-- place on every gear change), so a row may not hold a reference to it. Serialising here, at
|
|
-- record time, is what keeps a buffered row from silently re-reading a list that has since changed.
|
|
local pctBuf = {}
|
|
function PH.formatPercents(list)
|
|
if not list then return "" end
|
|
local n = table.getn(list)
|
|
for i = table.getn(pctBuf), n + 1, -1 do pctBuf[i] = nil end
|
|
table.setn(pctBuf, n)
|
|
for i = 1, n do pctBuf[i] = string.format("%g", list[i] or 0) end
|
|
return table.concat(pctBuf, "/", 1, n)
|
|
end
|
|
|
|
-- pcts / zone / cast are the three fields the offline analysis asked for (docs/
|
|
-- VAMPIRISM-MODELL-STAND §4). All three are optional: an older caller that does not supply them
|
|
-- still produces a well-formed line ("", "?" and 0), so the export never depends on every wiring
|
|
-- site having been updated in the same change.
|
|
--
|
|
-- cast=1 means a SPELL_GO_SELF for THIS spell id arrived shortly before this hit -- i.e. the player
|
|
-- CAST it. cast=0 means no cast announced it, which for a damaging spell is the signature of a
|
|
-- proc. That distinction is the whole point: hypothesis H2 says Vampirism is fed by the TRIGGER
|
|
-- PATH ("was this triggered by an aura") rather than by "is this an item proc", and the cast event
|
|
-- is the one observable in the client that separates the two paths for the same spell id. Without
|
|
-- it the offline classification has to be inferred from timing against Lightning Strike hits and
|
|
-- incoming damage, which is what the current analysis does and why it can only report AMBIG for
|
|
-- 13.4 % of Tidal Wave hits.
|
|
function PH.formatHitLine(row)
|
|
return string.format(
|
|
"HIT|t=%.3f|src=%s|dmg=%d|aoe=%d|P=%.4f|n=%d|pred_heal=%.4f|hp_before=%s|hp_after=%s|"
|
|
.."hp_max=%s|acc_total=%.4f|crit=%d|pcts=%s|zone=%s|cast=%d",
|
|
row.t, row.src, row.dmg, (row.aoe and 1 or 0), row.P, row.n, row.pred_heal,
|
|
tostring(row.hp_before), tostring(row.hp_after), tostring(row.hp_max), row.acc_total,
|
|
(row.crit and 1 or 0), row.pcts or "", row.zone or "?", (row.cast and 1 or 0))
|
|
end
|
|
|
|
function PH.formatIncomingLine(row)
|
|
return string.format("INC|t=%.3f|dmg=%.4f", row.t, row.dmg)
|
|
end
|
|
|
|
function PH.formatSelfHealLine(row)
|
|
return string.format("SELFHEAL|t=%.3f|heal=%.4f", row.t, row.heal)
|
|
end
|
|
|
|
-- Foreign healing landing on the player -- exactly the healing that contaminates a group /vf watch
|
|
-- window (see capture/incoming.lua's header comment), kept as its own line instead of folded into
|
|
-- SELFHEAL so it is measurable rather than invisible. Same shape/skip rule as formatSelfHealLine.
|
|
function PH.formatExternalHealLine(row)
|
|
return string.format("EXTHEAL|t=%.3f|heal=%.4f", row.t, row.heal)
|
|
end
|
|
|
|
-- Closes whatever hit is currently open (if any): stamps hp_after, formats and pushes its line,
|
|
-- clears the pending slot. Called from three places: the NEXT hit (below), the OnUpdate timeout
|
|
-- (PH.tick), and a forced flush (PH.flushNow/PH.disable) -- exactly one of these will ever close
|
|
-- a given pending row, since each clears it before returning.
|
|
function PH.finalizePending(state, hpNow)
|
|
local p = state.pending
|
|
if not p then return false end
|
|
p.hp_after = hpNow
|
|
PH.pushLine(state, PH.formatHitLine(p))
|
|
state.pending = nil
|
|
return true
|
|
end
|
|
|
|
-- fields: t, src, dmg, aoe, P, n, pred_heal, acc_total, crit, pcts, zone, cast -- everything
|
|
-- except the health readings, which this function supplies itself: hp_before = hpNow and
|
|
-- hp_max = hpMaxNow are both stamped HERE, at the same point, and never touched again; hp_after
|
|
-- comes later, at finalize.
|
|
--
|
|
-- fields.pcts is the LIVE source-percent list and is serialised HERE, not held. The row can sit
|
|
-- pending (and then buffered) for a long time, and that list is a recycled buffer refilled in
|
|
-- place whenever gear changes -- keeping the reference would make an already-recorded hit report
|
|
-- whatever the source set happens to be at flush time. Same reasoning as the health readings
|
|
-- above: a value that describes THIS hit is captured at this hit.
|
|
function PH.recordHit(state, fields, hpNow, tNow, hpMaxNow)
|
|
PH.finalizePending(state, hpNow)
|
|
state.pending = {
|
|
t = fields.t, src = fields.src, dmg = fields.dmg, aoe = fields.aoe,
|
|
P = fields.P, n = fields.n, pred_heal = fields.pred_heal,
|
|
acc_total = fields.acc_total, crit = fields.crit,
|
|
pcts = PH.formatPercents(fields.pcts), zone = fields.zone, cast = fields.cast,
|
|
hp_before = hpNow, hp_max = hpMaxNow, pendingAt = tNow,
|
|
}
|
|
state.totalHits = state.totalHits + 1
|
|
end
|
|
|
|
-- Force-closes a pending hit once it has sat open longer than PENDING_TIMEOUT with no follow-up
|
|
-- event to close it the normal way. Returns true if it actually closed one, for the tests.
|
|
function PH.tick(state, hpNow, tNow)
|
|
if state.pending and (tNow - state.pending.pendingAt) >= PENDING_TIMEOUT then
|
|
return PH.finalizePending(state, hpNow)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Zero/nil incoming damage or self-heal in a window is the common case (most windows have
|
|
-- neither) and is skipped rather than emitted as a line of noise -- an absent INC/SELFHEAL line
|
|
-- for a given HIT IS the "nothing happened" signal for offline analysis.
|
|
function PH.recordIncoming(state, t, dmg)
|
|
if not dmg or dmg == 0 then return false end
|
|
PH.pushLine(state, PH.formatIncomingLine({ t = t, dmg = dmg }))
|
|
return true
|
|
end
|
|
|
|
function PH.recordSelfHeal(state, t, heal)
|
|
if not heal or heal <= 0 then return false end
|
|
PH.pushLine(state, PH.formatSelfHealLine({ t = t, heal = heal }))
|
|
return true
|
|
end
|
|
|
|
function PH.recordExternalHeal(state, t, heal)
|
|
if not heal or heal <= 0 then return false end
|
|
PH.pushLine(state, PH.formatExternalHealLine({ t = t, heal = heal }))
|
|
return true
|
|
end
|
|
|
|
function PH.chunkName(sid, chunkIdx)
|
|
return "vampify_perhit_" .. tostring(sid) .. "_" .. tostring(chunkIdx)
|
|
end
|
|
|
|
function PH.chunkHeader(version, sid, chunkIdx, lineCount)
|
|
return string.format("addon=Vampify version=%s chunk=%d sid=%s lines=%d",
|
|
tostring(version), chunkIdx, tostring(sid), lineCount)
|
|
end
|
|
|
|
-- Pure half of a flush: builds the chunk's name and full text (header + every buffered line),
|
|
-- advances the chunk index, and empties the buffer -- all without touching ExportFile, so it is
|
|
-- testable without any WoW stub. Returns (nil, nil) when there is nothing to flush.
|
|
function PH.takeFlushText(state, version)
|
|
local lineCount = table.getn(state.buf)
|
|
if lineCount == 0 then return nil, nil end
|
|
local name = PH.chunkName(state.sid, state.chunk)
|
|
local header = PH.chunkHeader(version, state.sid, state.chunk, lineCount)
|
|
local text = header .. "\n" .. table.concat(state.buf, "\n") .. "\n"
|
|
state.chunk = state.chunk + 1
|
|
state.totalFlushed = state.totalFlushed + lineCount
|
|
VampifyConst.resetList(state.buf)
|
|
return name, text
|
|
end
|
|
|
|
-- ---- session-local singleton + disk flush --------------------------------------------------
|
|
--
|
|
-- The module's OWN default is still disabled -- every addon load starts VampifyPerHit._state at
|
|
-- PH.newState()'s enabled=false, and this file never reads VampifyConfig. What makes the channel
|
|
-- actually run every session is core/commands.lua's syncPerHit(), called on every
|
|
-- PLAYER_LOGIN/PLAYER_ENTERING_WORLD, which reads VampifyDB.perhitEnabled (core/config.lua,
|
|
-- default true) and calls PH.enable()/.disable() accordingly -- the same "module stays
|
|
-- config-agnostic, the wiring layer bridges to SavedVariables" split core/model.lua and the rest
|
|
-- of core/*.lua already use.
|
|
|
|
VampifyPerHit._state = PH.newState()
|
|
|
|
-- Flush when the buffer reaches this many lines, or this many seconds have passed, whichever
|
|
-- comes first -- the same two-trigger shape (flush-on-count / flush-on-timer) used by a companion
|
|
-- capture addon's own chunk-rotation library, the reference pattern this mirrors. Deliberately NOT
|
|
-- reusing that library itself: this channel needs neither its chunked wire protocol (nothing
|
|
-- ingests these chunks automatically) nor its multi-producer plumbing, and pulling it in would be
|
|
-- exactly the kind of heavy new subsystem this channel is meant to avoid.
|
|
--
|
|
-- SIZED FOR PERMANENT OPERATION (follow-up change request, 2026-08-22: default on, runs every session, not
|
|
-- just an armed-by-hand dev probe). LibEmpBus's own FLUSH_SEC=2 is right for ITS job -- feeding a
|
|
-- near-realtime backend watcher -- but nothing ingests these chunks automatically, and the addon
|
|
-- cannot delete old ones (no filesystem delete from Lua). A short interval that flushed whatever
|
|
-- was buffered, even one line, would turn "grind mobs one at a time so combat drops between
|
|
-- pulls" into a file per kill. 500/60s bounds a busy AoE pull to a handful of chunks (a line is
|
|
-- ~130 bytes; 500 lines is ~65 KB) while keeping the worst-case unflushed tail small if the
|
|
-- client vanishes without a clean logout (PLAYER_CAMPING/PLAYER_QUITING force a flush too, see
|
|
-- core/commands.lua). See the addon's PR notes for the resulting bytes/hour estimate.
|
|
PH.FLUSH_LINES = 500
|
|
PH.FLUSH_SECS = 60
|
|
|
|
local function flushToDisk()
|
|
if not ExportFile then return end -- SuperWoW only; harmless without it
|
|
local name, text = PH.takeFlushText(VampifyPerHit._state, VampifyConst.VERSION)
|
|
-- ExportFile appends .txt itself -- name must NOT already carry it (see core/const.lua's
|
|
-- writeErrors for the same gotcha).
|
|
if name then ExportFile(name, text) end
|
|
end
|
|
|
|
function PH.isEnabled()
|
|
return VampifyPerHit._state.enabled
|
|
end
|
|
|
|
function PH.status()
|
|
local s = VampifyPerHit._state
|
|
return {
|
|
enabled = s.enabled,
|
|
sid = s.sid,
|
|
chunk = s.chunk,
|
|
buffered = table.getn(s.buf),
|
|
totalHits = s.totalHits,
|
|
totalFlushed = s.totalFlushed,
|
|
}
|
|
end
|
|
|
|
-- time() is a WoW global (used the same way by a companion capture addon); math.floor(GetTime())
|
|
-- is the fallback for an environment that somehow has GetTime but not time(). Session id only
|
|
-- has to be unique enough to tell two /vf perhit on sessions' chunks apart on disk, not globally
|
|
-- unique.
|
|
function PH.enable()
|
|
if PH.isEnabled() then return false end
|
|
local sid = time and time() or math.floor(GetTime and GetTime() or 0)
|
|
PH.setEnabled(VampifyPerHit._state, true, sid)
|
|
return true
|
|
end
|
|
|
|
-- Finalizes whatever hit is still open and force-flushes the current chunk, THEN turns the
|
|
-- channel off -- so /vf perhit off never strands the last hit's hp_after in memory.
|
|
function PH.disable(hpNow)
|
|
if not PH.isEnabled() then return false end
|
|
PH.finalizePending(VampifyPerHit._state, hpNow)
|
|
PH.setEnabled(VampifyPerHit._state, false)
|
|
flushToDisk()
|
|
return true
|
|
end
|
|
|
|
-- Same finalize+flush as disable(), without turning the channel off -- for a point that needs a
|
|
-- GUARANTEED write regardless of the size/time thresholds (currently only /vf perhit off and a
|
|
-- real logout/camp -- see core/commands.lua's PLAYER_CAMPING/PLAYER_QUITING handling).
|
|
function PH.flushNow(hpNow)
|
|
if not PH.isEnabled() then return end
|
|
PH.finalizePending(VampifyPerHit._state, hpNow)
|
|
flushToDisk()
|
|
end
|
|
|
|
local function maybeFlush()
|
|
if table.getn(VampifyPerHit._state.buf) >= PH.FLUSH_LINES then flushToDisk() end
|
|
end
|
|
|
|
-- Finalizes a pending hit WITHOUT forcing a disk write -- for combat end. A hit right before
|
|
-- PLAYER_REGEN_ENABLED must not be left open across what could be a long idle gap until the next
|
|
-- fight (same reasoning as flushNow), but forcing a chunk to disk on every single fight is exactly
|
|
-- the "file per kill" permanent-operation problem FLUSH_LINES/FLUSH_SECS above exist to avoid --
|
|
-- several short fights' hits accumulate in the SAME buffer until one of those thresholds fires
|
|
-- naturally. maybeFlush() still applies: a fight big enough to cross FLUSH_LINES on its own closes
|
|
-- its own chunk immediately, same as it would mid-fight.
|
|
function PH.closeWindow(hpNow)
|
|
if not PH.isEnabled() then return end
|
|
PH.finalizePending(VampifyPerHit._state, hpNow)
|
|
maybeFlush()
|
|
end
|
|
|
|
-- fields: see PH.recordHit. Cost when the channel is off is one isEnabled() check and nothing
|
|
-- else -- no table built, no line formatted.
|
|
function PH.hit(fields, hpNow, tNow, hpMaxNow)
|
|
if not PH.isEnabled() then return end
|
|
PH.recordHit(VampifyPerHit._state, fields, hpNow, tNow, hpMaxNow)
|
|
maybeFlush()
|
|
end
|
|
|
|
function PH.incoming(t, dmg)
|
|
if not PH.isEnabled() then return end
|
|
if PH.recordIncoming(VampifyPerHit._state, t, dmg) then maybeFlush() end
|
|
end
|
|
|
|
function PH.selfheal(t, heal)
|
|
if not PH.isEnabled() then return end
|
|
if PH.recordSelfHeal(VampifyPerHit._state, t, heal) then maybeFlush() end
|
|
end
|
|
|
|
function PH.extheal(t, heal)
|
|
if not PH.isEnabled() then return end
|
|
if PH.recordExternalHeal(VampifyPerHit._state, t, heal) then maybeFlush() end
|
|
end
|
|
|
|
-- Driven from the SAME 0.25s OnUpdate throttle core/commands.lua already runs everything else
|
|
-- from -- no new frame, per the project's OnUpdate-allocation rule. dt (elapsed since the last
|
|
-- tick) feeds the time-based flush trigger; hpNow/tNow close a pending hit that never got a
|
|
-- follow-up event (PH.tick's PENDING_TIMEOUT).
|
|
function PH.onTick(hpNow, tNow, dt)
|
|
if not PH.isEnabled() then return end
|
|
PH.tick(VampifyPerHit._state, hpNow, tNow)
|
|
local s = VampifyPerHit._state
|
|
s.elapsed = s.elapsed + (dt or 0)
|
|
if s.elapsed >= PH.FLUSH_SECS then
|
|
s.elapsed = 0
|
|
flushToDisk()
|
|
end
|
|
end
|