v2.1.0
What's new
Garu Combat Text
v2.1.0 (2026-10-18)
Full Changelog Previous Releases
- Keep Thorns counted through the fight, as the trace showed it
The first trace with Thorns up showed three things the damage-shield rule got
wrong, which is why it only came right once the fight was over and the meter
corrected it.
Order. The shield's hit on the attacker is reported at once; the report of the
melee blow on you that set it off comes 0.30-0.36 s later, every time -- as
melee always is on this client. The rule waited 0.15 s for the blow, so every
shield hit gave up just before it came. It now waits 0.6 s, and each blow claims
one waiting shield hit, oldest first, so two mobs striking give two. A blow
reported first still claims a shield hit within 0.15 s. A blow absorbed or
blocked whole still counts: it still sets Thorns off.
Hidden buffs. Thorns was placed at 26.20 and then not even considered at 28.24,
still up. In combat the game hides your buffs from addons, and the reader stopped
at the first hidden one, so a re-read mid-fight found nothing and let Thorns go.
Now the known shields are asked for by spell (C_UnitAuras.GetPlayerAuraBySpellID)
before any list is walked; the walk steps over hidden buffs instead of stopping;
and in combat no shield is let go -- only out of combat, when the buffs can be
read and it is really gone. What is read goes to the recorder as A lines.
The blow that kills. A mob that dies in the shield's moment never has its blow
on you reported, so a waiting shield hit on your target that cleared in that
moment is the shield's anyway. That also needed the fight to be known after a
/reload in the middle of one, when no PLAYER_REGEN_DISABLED comes: the state is
now asked of the game at PLAYER_ENTERING_WORLD.
The recording is the third fight in tests/fights/, replayed every run: 19 of 19
hits placed. The replay learned A lines, a /reload restarting the recorder's
clock, and PLAYER_ENTERING_WORLD on a P line. Engine tests cover the measured
gap, two mobs, a hidden buff ahead of a shield, Thorns found by spell with the
list hidden, and a shield kept through unreadable combat then let go after.
Six more mutants, 39 in all.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Count Thorns and other damage shields
Thorns damage went uncounted on both clients, for two different reasons.
Forever: a damage shield's hit has nothing of yours to be matched to. Thorns was
cast minutes before, on yourself; its damage lands on whatever melee-hits you,
in that same moment. So a Nature hit with no cast, DoT or swing to explain it
was taken for someone else's. Now, while a damage shield of yours is up, an
otherwise unexplained hit of its school on an enemy is the shield's when it
comes in the moment (0.15 s) a melee hit landed on you -- or, the order of the
two not being fixed, when that hit follows within the moment. Your casts and
DoTs are asked first, so a Wrath landing then keeps its hit. Which shields are
up is read from your buffs (C_UnitAuras, checked for secrecy first, as FCT
does): Thorns, Lightning Shield and Retribution Aura by spell, every rank as
PrettyTooltip's SpellRanks lists them -- the recorder showed Thorns' description
not loading when cast -- and any other by its text ("damage to attackers", "any
enemy that strikes"). Only one you cast counts: another druid's Thorns on you,
or a party paladin's aura, is theirs. A spell hitting you does not set it off.
Classic: the combat log reports a damage shield as DAMAGE_SHIELD, which the
feeds never listened for. Damage Dealt now counts it, Actions its resisted form,
and Damage Taken an enemy's shield hurting you.
Engine tests cover Thorns in both orders, without a blow on you, after a spell
on you, beside a Wrath landing that moment, someone else's Thorns, and a shield
known only by its text; the load test sends DAMAGE_SHIELD through the Classic
combat log. Six more mutants, 33 in all.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Count your target, or every enemy as one set for the fight
Damage Dealt and Actions were per target only. Each now has Count: Your target,
as before, or Every enemy -- everything you hit, as one set of totals for the
fight, kept through it and for Hold after combat (8 s) after, then faded. The next
fight's first hit clears it, not combat starting: the opening blow is reported
just before the game says combat began, and clearing then would lose it. Damage
Taken and Healing were whole-fight already.
The engine carries it (Meter.AddScope): a feed counting every enemy keys its
stream once for the fight rather than by your target, keeps no per-target memory,
and a target change no longer re-switches a stream to the key it already has --
which, with no memory to restore from, would have wiped it.
Classic: the combat log already reports every hit you land; the feed stops
throwing away the ones not on your target (anything but yourself counts).
Forever: other enemies' hits are only reported on their nameplates, so a feed
counting every enemy reads those too -- not your target's own plate, which
reports the same hit again, nor a friend's. Casts and DoTs already remember the
unit they were aimed at, so a DoT ticking on the mob you left is still yours.
Two rules are new. A swing remembers its unit, so it cannot take a hit on another
mob. An area spell (its text says nearby enemies, all enemies within, up to N
targets...) claims one hit on each enemy in its cast's own moment, and an area
DoT ticks on each enemy on its own beat. And once the fight is over each spell is
set to Blizzard's damage meter's figure, which for every enemy is exactly the
feed's: every hit, the killing blows, and whatever the matching missed or took
wrongly. Hits on enemies with no nameplate shown cannot be seen live; the
meter still puts them in afterwards.
Engine tests cover a DoT on a mob you left, one hit reported twice with a second
bolt in flight, a friend's plate, an Arcane Explosion on three mobs, a swing that
must not take another mob's hit, per-target mode ignoring plates, another mob's
resist marked as not on your target, and the meter's totals after the fight. The
load test follows every-enemy totals across a target switch and a nameplate,
through the meter's correction to a fresh start, and on Classic counts combat-log
hits on any enemy only when asked. Nine more mutants, 27 in all.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Keep the settings window out of chat
Unlocking the anchors, resetting and copying each printed a line to chat, so
working the window left a trail there. The window already shows what changed --
its header buttons, the page, and now the info panel for a copy -- so none of
them print any more. Typed commands still answer (/gct lock, /gct reset, /gct
test), since nothing else would tell you they worked, and the probe's report
stays in chat, which is the whole of what it does.
The load test works the window -- anchors, preview, a copy, a checkbox, a reset
-- and fails on any line written to chat; a mutant that puts one back is caught.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Copy any feed's settings to any other, or all; drop the page blurb
Copying was fixed pairs: each feed could only take one particular other's look,
always mirrored, always with its position. A feed page's header now has Copy
to...: every other feed, and All other feeds. Two choices ride along and are kept
between uses -- Position too, and Mirror left to right for two feeds that sit as
a pair. Layout and text go across; what a feed counts (thresholds, pets, mana)
stays, since that means something different on each. A line in chat says what
went where.
The grey description under each page's title is gone. Its text still shows in
the info panel whenever no setting is hovered.
The load test copies to one feed, to all, with and without the position, and
mirrored, on every client including the bare one, where the menu is the old
dropdown. Two more mutants guard it. The stub stops answering unknown lowercase
globals with a stub: the test's own cleared lastMenu had been reading back as an
empty menu, which hid that the bare client never reached its fallback.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Rebuild the settings window in FlareUI's style
The old window was five tabs of controls stacked one under another, four of them
repeating the same fifteen, with long notes inline between them. It now follows
FlareUI (Malkkav, MIT), the way the other addons on this client look: the pages
in a list on the left with a dot for whether each feed is on, the page's
settings in the middle under headings that fold, and on the right what the
hovered setting does. Blizzard's own controls throughout -- checkbox, the
Settings stepper for choices, the minimal slider with a box to type a value --
in FlareUI's bronze, over its tiled background.
More compact: the explanations moved off the page into the info panel, labels
are short, on/off settings sit two to a line, Enabled became the switch in each
page's header, a feed that is off shows one card with Turn on, and Unlock and
Preview sit in the window's header. The client's note (worked out on Forever,
post-fight summary, grouped by school) is a single bronze strip; hover it for
the whole story. Search, at the foot of the list, finds a setting on any page
and names the page it is on. Tab order from 1.16 is kept.
Pages are data now: each row says what it is, what it does, how to read and
write its value and when it applies, and the four feed pages share one builder,
so a setting exists once instead of four times. The window is drawn from that
data after every change, which is how a setting that stops applying goes and
comes back. Blizzard's damage-number toggle moved to a General page, with the
Forever tools (probe, recorder), the post-fight summary's settings and Reset,
which now asks first.
Along the way: a reset refreshed only two of the four feeds and left Damage
Taken and Actions looking as they were until a reload; /gct lock and /gct test
now redraw an open window.
The load test drives the window the way a player would -- every page, every
checkbox clicked and clicked back, every choice picked, every slider dragged to
both ends and typed into, search, a feed off and on, reset -- on Forever, on
Classic, and on a bare client without Blizzard's newer templates so the
fallbacks run too: 93, 91 and 91 controls. Four more mutants guard it. The stub
grew honest along the way: frames keep their shown, checked and typed state and
their scripts, sliders report through OnValueChanged, and template children
exist only where a template made them.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Settle the killing blow from the damage meter after the fight
The blow that kills your target was never counted, and no matcher could have
counted it: it is never reported. In the recorded kills the killing swing's
PLAYER_SWING and the target clearing arrive in the same frame, and the
UNIT_COMBAT that would have followed -- a melee hit is reported about half a
second after its swing -- has no "target" left to arrive on. A kill by a DoT
tick was reported, in that same frame.
The amount is not lost, only late. C_DamageMeter hides its figures during
combat and gives the fight's totals once it ends, so a target that clears in
combat within 0.1 s of a blow of yours is noted as a kill by that attack, and
when the meter speaks, whatever it counted for that attack beyond what the feed
showed is credited to the dead unit -- on screen if it is still selected, else
into what is remembered for it (Meter:BumpGUID), so it is there on the next
click. Only the killing attack is topped up, only by what is missing (a kill
whose blow was reported adds nothing), and not at all when the gap is more than
three of that attack's largest hits: that means other mobs' damage is mixed in,
and splitting it would be a guess. Pets are left out; the meter keeps them apart.
The recorder now notes kills (K), the meter's figure beside the feed's (M), and
any report on the dead unit's nameplate, which would be a cheaper live fix if it
turns out to exist.
Both recordings replay with every hit placed (8 of 8, 18 of 18). Engine tests
cover the settled kill, the reported kill, the refused gap and a target switch
that is not a kill; a load test follows a kill through to the corpse's total.
Three more mutants guard them. The harnesses also gain a real wipe(): the load
test's catch-all stub had been answering it with a do-nothing.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Keep the trace extractor with the tests
Turning a /gct trace recording into a replayable fight is the step that feeds
tests/fights/, so it lives beside them rather than in a session scratchpad.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Widen the melee window to what this client actually does
The first /gct trace from a real fight cleared the suspect and found the cause.
PLAYER_SWING arrives as (weapon speed, swing type) -- "3.107 0" from a 3.1 s
weapon -- so the swing type was being read from the right argument all along.
What was wrong was the window: here a melee hit lands 0.48 to 0.52 s after its
swing is reported, and FCT's 0.5 s window put half of all melee on each side of
the line. One hit of 43 was placed, one of 47 was not.
One second still sits below any weapon's swing interval, and a swing is only
ever claimed once, so it cannot take two hits.
The recording itself goes into tests/fights/ and is replayed through the current
matcher on every run: every hit on the target must be placed. All 8 now are --
Moonfire's two openers and four ticks, and both swings. Future recordings drop
in beside it, so the matcher keeps being checked against what the game really
sent rather than against fights written by hand. Cast lines now also record a
spell's duration and whether it heals, which the replay needs to rebuild its
description.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Add a fight recorder, and keep the tests in the repo
The first real /gct probe settled the question the port stood on -- UNIT_COMBAT
amounts are readable in combat, on the target and on you -- and raised two more:
3 of 5 hits on the target went unplaced, and only one swing was seen in a fight
with melee in it. Totals cannot say why. Suspect number one is PLAYER_SWING's
payload: which argument carries the swing type was taken from FCT, never seen
here, and reading the wrong one would drop swings silently and leave their hits
unplaced.
/gct trace records every cast, every swing with its payload exactly as it came,
every UNIT_COMBAT report, and what the matcher made of each, into the saved
settings. A /reload writes it to disk, where the fight can be read back and the
matcher fixed from evidence.
The probe also put "casts seen: 0" beside a placed cast: the opening cast lands
before combat starts, so the reset at PLAYER_REGEN_DISABLED counted it in the
previous fight. The opener now counts in the fight it opened.
The offline checks move from a session scratchpad, where the last checker was
lost, into tests/: run.py compiles every file under LuaJIT, replays the matcher's
scripted fights, loads the whole addon against a stubbed game as both Forever
and Classic, then breaks the addon seven ways on purpose and fails if any
breakage goes unnoticed. The runtime installs into tests/.lupa (ignored); tests/
is left out of the packaged addon.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Bring live combat text back to Forever, on UNIT_COMBAT
The post-fight summary rested on a conclusion that was wider than what had been
measured. The addon combat log never fires and C_DamageMeter hides its figures
in combat -- both still true -- but UNIT_COMBAT was never tested, and it is
exactly what is needed: the event Blizzard's portrait hit-numbers use, live and
unhidden, carrying each hit's amount, school, crit flag or kind of miss on your
target and on you. Forever Combat Text by Tarolg (MIT) runs on it.
What it does not say is who made a hit or with which spell. Everything about
your own side is readable, though -- your casts with their spell, your weapon
swings, and each spell's own description -- so UnitCombat.lua matches each hit
on your target to them by time and school: a DoT tick on its beat, then the
oldest cast still in flight, then an unbeaten DoT, then a swing (including one
reported just after its hit), then your pet. The windows are the ones FCT
measured in real fights. It is worked out, not read: near-exact solo, and a
teammate's same-school hit inside your window can be taken for yours.
The feeds subscribe and Bump their meters exactly as their combat-log handlers
do, so the whole live apparatus returns: fades, rolling windows, per-target
memory. Damage Dealt is per spell again with crits, schools and mana spent;
Actions is back; Damage Taken is per school, since the attacker's spell is not
reported; Healing Received counts every heal on you, your own on their spell
and everyone else's on one line.
A DoT starts on a 3 s beat guess until two ticks are measured, so a first-fight
tick landing beside a bolt in flight does not swap the two. A tick-only spell
never waits for a direct hit, or a Corruption cast would take the bolt cast
after it. A hidden amount still uses up its cast but adds nothing, since a
secret can be drawn but never summed.
/gct probe now reports, per fight, how many hits came readable and how each was
placed. The summary stays as a one-line fallback (FORCE_SUMMARY) in case the
probe finds amounts hidden in combat on this client.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - Replace the two guesses about this API with what it actually returns
/gct probe over a real fight answered both open questions, and both answers were
no. Delete the speculative code and say plainly in the UI what is not available,
rather than leaving feeds that quietly show the wrong thing.
There is no per-target breakdown. EnemyDamageTaken does list one source per enemy
-- keyed by creatureID, with sourceGUID nil, which is why the GUID-keyed capture
found nothing -- but that source's spell rows collapse into a single entry with
spellID 0 carrying the enemy's whole total, attributed to whoever dealt it.
Per-enemy totals, no spell split, and these feeds are per-spell. So every corpse
from a pull shows the pull, and CaptureByTarget goes.
Healing received is worse: combatSpellDetails.unitName is blank on a source's own
rows -- it is filled in only on the receiving side, which is how EnemyDamageTaken
names the damager. Nothing can be matched against it, so the filter never
matched and the feed was silently empty. There is no healing-received meter to
fall back to either; the enum has HealingDone and Hps and nothing incoming. The
feed now reads the local player's own HealingDone rows, which is the healing you
CAST -- the same number when you heal yourself, and not the same thing at all in
a group. The tab is retitled "Healing Cast" on this client and both the note and
/gct status say so, because a feed labelled "received" showing cast numbers is
the kind of wrong that never gets noticed.
The three capture paths collapse into one DM.Capture(meterType): the local
player's rows, whichever direction that meter happens to record.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Show the summary against what you killed, for as long as you want it
A post-fight summary was still being displayed like live combat text: it
appeared the instant combat dropped, wherever you happened to be looking, and
sat there until the next pull. That reads as though the numbers belong to
whatever you are pointing at now rather than to the fight that just ended.
Show it like the report it is. Nothing appears when combat drops; the summary
appears when you select what you were fighting, holds for ns.db.summary.holdSecs
(15s by default, configurable on the Damage Dealt tab), then dims out. Selecting
something else shows that one's figures, and going back to the first before its
window closes brings it back -- each target carries its own window, started the
first time you look at it, so a corpse you get to late is still readable.
The increment machinery goes with it. Diffing successive polls only ever made
sense while the feeds were pretending to be live; a fight's totals arrive once,
complete, so a capture is now just "read the rows and keep them". That also
retires the unverified baseline arithmetic: SessionType.Current is per-fight, not
a running tally -- the API carries a separate Overall session for that, and the
one probe we have read Current 147 against Overall 501.
Damage Dealt additionally asks EnemyDamageTaken for a per-enemy split, so two
corpses from one pull can show their own numbers. That part is reasoned about
rather than measured: if the session comes back in a shape it does not
recognise, every corpse shows the fight's total, which is what they did before.
/gct probe dumps the session so one real fight can settle it -- and the same dump
answers whether spell rows name their other party, which is the only thing
holding up healing received.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Only poll the damage meter while a finished fight is settling
The 4Hz sweep ran forever, in and out of combat. Nothing is readable during a
fight, and once the numbers land they don't change again until the next fight
ends, so almost every call was discarded. PumpIncoming is the expensive one: it
walks every source in the session, which in a raid is a great many calls a second
to throw away.
The ticker now only runs inside a four-second window opened by
PLAYER_REGEN_ENABLED, which also replaces the fixed 0.2/0.8/1.6/3.0s poll volley
it used to schedule -- same coverage, less machinery. The DAMAGE_METER_* hooks
still catch anything outside the window.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Don't blank the summary when you change target
Targeting anything made Damage Dealt disappear. PLAYER_TARGET_CHANGED calls
meter:Retarget, which swaps the stream to whatever was stored against the new
target's GUID -- nothing, for a fresh target -- so the feed emptied on the spot.
That is right when the numbers are per-target, which is what the live combat log
gives. It is wrong in summary mode, where the figures belong to the FIGHT: the
meter reports what happened, not who you are currently clicking on. Retarget and
the focus equivalent are now live-mode only.
The tab heading reads "Damage Dealt (this fight)" there rather than "(per
target)", and "Remember each target's totals" is hidden, since neither means
anything when the data isn't split by target.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Make the Forever build an honest post-fight summary
It was still wearing clothes built for a live feed: rolling windows, hold timers
and fade curves, all driven off combat start and end, with the data arriving at
exactly the wrong moment for every one of them.
Summary mode (ns.SummaryMode, set wherever C_DamageMeter exists) now switches that
apparatus off rather than fighting it. Feeds never fade and the healing window
never rolls; a fight clears them when it starts and fills them when it ends, and
the figures stay put until the next fight. That removes both reported symptoms at
the source rather than by retuning timings: Damage Taken faded instantly because
its hold timer began counting when combat dropped -- the very moment the numbers
arrived -- and Damage Dealt stuck forever because nothing ever cleared it.
The live path is untouched, so the Classic clients keep every feed exactly as it
was, fades and all.
Also here:
* ns.CombatDuration uses the meter's own session length on Forever. It used to
time from the first hit we saw, and in summary mode we see no hits.
* The rolling-window and keep-after-combat sliders are hidden in summary mode,
and each affected feed's page says what the client actually allows.
* DM.IsUsable respects IsDamageMeterAvailable; /gct status reports which source
is feeding the addon and how long the last fight ran.
Removed, now that the question they existed to answer is settled: Probe.lua and
its slash branch, the combat-log diagnostics (/gct debug, DiagBump, ns.registered)
and the DiagBump calls in EnemyText. /gct status replaces them at a fraction of
the size.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Treat the damage meter as a post-fight summary, because that is what it is
The live sample answered it: out of combat the totals read fine (cur=147 ovr=501),
and the instant the fight starts every one of them stops being readable for the
whole 12 seconds. The client withholds the meter's numbers during combat and
releases them when it ends. Neither Current nor Overall moves mid-fight, so live
combat text is not possible from this API -- no amount of polling or event wiring
changes that.
So the feeds stop pretending. Each fight clears them on PLAYER_REGEN_DISABLED, and
PLAYER_REGEN_ENABLED polls a few times over the next three seconds because the
session isn't finalised the moment combat drops. The result is a summary of the
fight just finished rather than a running commentary during it.
This also explains the two symptoms that came with the report. Damage Taken
"faded immediately" because its post-combat hold timer starts when combat ends,
which is exactly when the data finally arrives. Damage Dealt "stuck forever"
because that feed has persist set, so nothing ever aged it out -- clearing at the
start of the next fight is what it was missing.
The live sampler now separates SECRET from nil and tracks IsDamageMeterAvailable,
since "withheld" and "absent" would have meant different things.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Re-baseline on target change instead of forgetting the totals
DM.Reset wiped the remembered per-spell totals, which looks harmless and isn't:
the next poll read the session's running total against a baseline of zero and fed
the entire fight in as one delta. Switching target dumped the whole session's
damage into the new target's feed.
It now flags the state instead, and the next poll adopts the current totals
silently and counts from there.
Also adds /gct probe live: samples DamageDone's Current and Overall totals once a
second for 12s alongside your combat state. The reported symptom -- nothing during
a fight, everything at the end -- says the session is finalised rather than
updated live, and if neither total moves mid-fight then live combat text is not
possible from this API at all and these feeds are post-fight summaries. Worth
knowing before tuning anything else.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Rebuild the feeds on C_DamageMeter
The probe confirmed the addon combat log is gone on Forever and showed what
replaced it. C_DamageMeter reports per-fight AGGREGATES: a session carries
combatSources, the player's row is marked isLocalPlayer (no GUID comparison
needed, which is just as well), and a source's combatSpells give spellID plus a
running totalAmount.
Rather than rewrite Meter around absolute totals, DamageMeter.lua converts them
back into the increments Meter already speaks: it remembers the last total per
spell and feeds the difference. Rolling windows, fades, sorting, the radial and
stack layouts all keep working untouched, so the feeds look as they did on a
client with a combat log. A session's totals going backwards means a new fight,
which drops the remembered figures rather than emitting one huge delta.
Per feed:
* Damage Dealt -- DamageDone, the local player's rows. Works.
* Damage Taken -- DamageTaken, same shape. Works, minus the avoid rows.
* Healing Recv -- the shaky one. HealingDone is keyed by the HEALER, so this
walks every source and keeps the spell rows whose combatSpellDetails.unitName
is me. If the client leaves that blank the feed stays empty, which is the
right failure: showing someone else's healing as yours would be worse.
* Actions -- switched off. Dodges, parries and misses are combat-log
facts and this API reports damage, not misses. The options tab says so
rather than leaving the feed silently dead.
Also gone for good, because no field carries them: per-hit crits, spell schools
and mana spent.
The CLEU handlers are kept behindif not ns.UseDamageMeter, so the older
Classic clients are unaffected and keep every feature.
ns.Truth and ns.SpellName move into Core -- the first was written for Unbuffed
and would have thrown nil on the first poll here.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Let /gct probe <type> skip the API survey
The survey alone fills the chat frame, so the session/source/spell dump scrolled
off before it could be read. Naming a meter type now prints only the data half.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Add /gct probe to survey the C_DamageMeter API on this client
/gct debug proved the addon combat log is gone on Forever: CLEU is a valid,
registered event that delivered nothing to the addon or to a bare probe frame
across 4s of real combat, and C_DamageMeter is present. The feeds have to be
rebuilt on that API.
Designing the rewrite from a retail addon's use of C_DamageMeter would be a guess
about a different client, and guesses about this one have not been going well. So
before writing any of it: /gct probe dumps what is actually there -- the function
list, the DamageMeterType and DamageMeterSessionType enum members, which
DAMAGE_METER_* events are valid, and the real field names of a session, a combat
source, and a spell entry, including combatSpellDetails.
/gct probe [damage|healing|taken|enemy] picks the meter type; damage by default.
Fields are printed through a secret-safe formatter, since the secret test has to
precede tostring or # on any value.
Probe.lua, its TOC line and the slash branch all come out once the feeds are
ported -- this is reconnaissance, not a feature.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Make /gct debug self-verifying
The first run reported zero combat-log events, but could not distinguish the three
things that produce that: the client not delivering the event, our dispatcher
swallowing it, or the player simply not fighting during the window.
Now it samples combat state once a second, registers the event on a private bare
frame as a control, reports C_EventUtils.IsEventValid, and says whether
C_DamageMeter exists -- which is what 12.x gives addons in place of the raw combat
log. The verdict line names whichever of the three it was.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Route combat-log events by object flags instead of GUID comparison
Every feed gated on a GUID compare -- "is the source me", "did this land on my
target" -- against UnitGUID("player") / UnitGUID("target"). On Midnight UnitGUID
can come back as a secret, a secret never equals a plain string, so every event
failed its filter. No error, no output: the preview still drew because preview
data is handed straight to the feeds, which is exactly the reported symptom.
The combat log already answers both questions in its own object flags, which are
plain numbers and cannot go secret. ns.SourceIsMine / ns.UnitIsMe /
ns.UnitIsMyTarget / ns.UnitIsMyFocus read those, and ns.SameGUID keeps the old
compare as a fallback for clients or events that don't set the special flags --
COMBATLOG_OBJECT_TARGET in particular may not be set on Vanilla-era content.
ns.SameGUID also reports "no match" for a secret rather than inspecting one,
which would throw.
Applied to all four feeds plus both mana-attribution paths, which had the same
compare. The per-file flag constants are gone; they live in Core now.
Also adds /gct debug: counts combat-log events for 15s across seen / passed the
source filter / routed to a feed, and reports whether CLEU registered at all and
whether the player and target GUIDs are secret. That distinguishes "no events
arrive" from "a filter drops them" without guessing, if this isn't the whole fix.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Never write a CVar during combat lockdown
Writing a gameplay CVar is protected in combat, and doing it from tainted addon
code pops Blizzard's "GaruCombatText has been blocked from an action only
available to the Blizzard UI" dialog. ApplyBlizzardFCT runs from
RefreshEnemyText, which fires while fighting.
This is fallout from the previous commit rather than a long-standing bug: before
it, the pre-Midnight CVar name simply did not exist on Forever, so the write
failed harmlessly inside its pcall. Resolving the "_v2" spelling made the call
reach a real CVar, and with it the protection.
ns.SetCVar now records the value it wants and applies it through RunWhenSafe --
immediately when out of combat, otherwise at PLAYER_REGEN_ENABLED -- with a
single flusher however many CVars queue up. It also skips the write entirely when
the CVar already holds the value, which is what most calls turn out to be, so the
protected call happens about once per session instead of on every refresh.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Handle Midnight secret values in the severity scaler
UnitHealthMax comes back as a secret on WoW Forever once execution is tainted by
the addon, and Meter.lua compared it directly -- "attempt to compare local 'maxhp'
(a secret number value)" on every OnUpdate tick of every stream.
A secret can be stored and passed around, but ordering comparisons on one throw
and arithmetic on one yields another secret, so a font size derived from max HP
could never have reached SetFont anyway. When max HP is secret, scale severity
against the largest total currently in the feed instead: that is our own
combat-log tally, so it is always a plain number. Scaling then reads as "relative
to the biggest number on show" rather than "relative to the target's health pool",
with the top line always at max size -- the closest thing to the original intent
that a secret max HP leaves available.
ns.IsSecret wraps issecretvalue, which only exists on the Midnight-era clients.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Port to WoW Forever (interface 16001, Midnight 12.x API)
Forever serves Classic content from the Midnight client: it reports a Classic
WOW_PROJECT_ID but only exposes the modern namespaced API and the retail widget
set. The TOC now declares 16001 alongside 20505, so one set of files covers both
clients, and every call site feature-detects rather than branching on project id.- Spell icons go through ns.SpellTexture, which prefers C_Spell.GetSpellTexture;
the GetSpellTexture global is gone on Midnight, so every combat-text line was
silently losing its icon. - The settings dropdowns were UIDropDownMenuTemplate, which Midnight removed
outright. They are now DropdownButton/WowStyle1DropdownTemplate driven by
MenuUtil.CreateRadioMenu, which tracks the selection and paints the button text
itself. The legacy branch stays for clients with no MenuUtil. - ns.SetCVar routes CVar writes through C_CVar and picks the "_v2" spelling
Midnight renamed the floating-combat-text CVars to. It checks the name exists
first, because SetCVar throws on an unknown one -- that is what skips
floatingCombatTextCombatDamageAllAutos, which is pre-Midnight only.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Spell icons go through ns.SpellTexture, which prefers C_Spell.GetSpellTexture;
This mod has no additional files

