Recount - Revived

Recount is a graphical damage meter written by Cryect. Recount (Preservation) is an attempt to preserve it and keep it running moving forward
Back to Files

Recount-v1.20.0

File nameRecount-Recount-v1.20.0.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 18, 2026
Downloads
3.8K
Size
474.8 KB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9241106
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 11.2.7
  • 5.5.4
  • 5.5.3
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

Recount Changelog

[v1.20.0] (2026-10-18) - Totems

New

  • A Totems mode: how many totems each shaman dropped, and which. Requested on Discord (#recount-revived, 2026-10-16): non-damaging totems such as Windfury or Strength of Earth never appeared anywhere, because the damage and healing modes skip a pet that did neither. New TrackerModules/TrackerModule_Totems.lua, fed from Recount:SpellSummon on every SPELL_SUMMON. A summon counts when its caster is a player whose class, from GetPlayerInfoByGUID, is SHAMAN, and its spell is a totem. The client has no flag marking a spell as a totem, so the module keeps one spell id per totem (36, any rank, Classic through MoP plus Season of Discovery's Decoy Totem rune, each checked against Wowhead's data for its flavour) and on a summon asks the client for each one's name with C_Spell.GetSpellName or GetSpellInfo; a summon counts when its spell name is one of those. The client loads some spell data on demand (Classic Era documents C_Spell.IsSpellDataCached and C_Spell.RequestLoadSpellData in Blizzard_APIDocumentationGenerated/SpellDocumentation.lua), so an id that exists but has no name yet is asked for with RequestLoadSpellData and retried on the next summon, instead of the table being built once and that totem going uncounted for the session (Peer Review, thread 9df105ed, F2). Both functions are feature-detected; without DoesSpellExist an id the flavour lacks is retried on every summon, at the cost of one lookup. Whether a spell's name can in fact be missing before its data loads is not verified. The totem-name check runs after the player and shaman checks, so other classes' summons never pay for those lookups (Peer Review, thread 9df105ed). Ranks share a name, so one id covers them all, and the client's own names make it work in every language. An id a flavour lacks has no name there and is skipped. This replaced a first version that counted every shaman summon except Feral Spirit, so an engineering dragonling or trinket summon showed as a totem (Peer Review, thread cf1a2066, R2). That the combat log's summon spell name equals the client's spell name for every totem on every flavour is not verified in game, nor what Wrath's Call of the Elements logs. An elemental totem's elemental is summoned by the totem, not the shaman, so it is not counted. The detail view lists each totem, broken down by the summoned unit's name. A totem never starts a fight, since totems go down before the pull. With Record Combat Only on, a totem dropped out of combat is held back and counted only if a fight starts within 120 seconds of it (the module wraps Recount:PutInCombat to flush the held totems), so totems dropped while travelling are never recorded. Held totems older than that are dropped from the list as new ones arrive, and a data reset (it wraps Recount:ResetDataUnsafe) forgets them. With Record Combat Only off, a totem counts at once, as every other mode records out of combat. Found by Peer Review on the self-audit (thread cf1a2066). Loaded by every TOC, appended after Threat so existing mode positions do not move; short tab name Tot. Retail on 12.0 and later has no combat log for addons, so the mode stays empty there. Not yet verified in game.
  • Totems are credited to their shaman like pets, in more modes. Asked by the operator on 2026-10-16: "how about we just treat totems like pets, and attribute the right totem to the right shaman?" Before this only Damage, DPS and Healing merged a pet into its owner, so Mana Spring's and Mana Tide's mana, a cleansing totem's dispels and Healing Stream's overheal sat on a totem combatant the bars never show. New Recount:MergedPetAmount in Recount_Modes.lua sums a field over the owner and every pet when Merge Pets w/ Owners is on, and Recount.MergedPetBreakdown exposes the existing per-pet breakdown merge (rows labelled with the pet's name, e.g. "Mana Spring (Mana Spring Totem)"). Overhealing Done, the Healing detail's overheal pane and its tooltip, Mana Given (TrackerModule_PowerGains.lua) and Dispels (TrackerModule_Dispels.lua) now use them, so a Felhunter's dispels count for its warlock too. With the option off, every mode reads the owner alone, as before. That the client reports a cleansing totem's removal as a SPELL_DISPEL from the totem is not verified for any flavour, so the CurseForge notes leave cleansing totems out until it is seen in game (Peer Review, thread 9df105ed, N1). Not yet verified in game.
  • Pet threat, and a Live Threat mode. Asked by the operator on 2026-10-17: "people solo with pets need to keep their threat below pet, so they have to see it", and "we need 2 types of threat meters, one that keeps track of it historically, and one that resets every fight, like threatclassic2". TrackerModule_Threat.lua now polls pet, partypetN and raidpetN beside their owners, keyed as the combat log stores a pet (PetName <OwnerName>, with Name-Realm for an owner from another realm). The existing Threat mode keeps each one's peak per fight and overall, against their own target. New Live Threat (short tab name Live, registered through the new Recount:AddOptionalMode, so it follows the Threat module's checkbox and is appended after every existing mode, keeping saved positions): everyone's threat against your target, or your target's target when you target a friend, retaken every poll and cleared when a fight starts; its tooltip gives the threat and the client's percentage toward pulling aggro. The live readings are held in a weak table outside the saved data, so a reload never brings back a stale list. Because it reads the present, not the selected fight, Live Threat is listed in the new Recount.LiveModes, which me:RenderPane and Recount:ReportData in GUI_Main.lua read (one HasDataFor helper) to give a bar and a report line to someone with no row in the selected fight; before that, with an older fight selected, anyone missing from it had no live bar (Peer Review, thread ef07ad9f, F6). Both threat modes list pets as their own bars even when Merge Pets is on or the Pet filter hides pets (Recount.PetShownModes, read by Recount:ShowsCombatant in GUI_Main.lua): a pet's threat is compared with its owner's, not added to it. The poller has no flavour guard of its own; whether UnitDetailedThreatSituation gives it a reading on Retail Midnight or Forever is not verified. That UnitName("pet") matches the name the combat log gives the pet was confirmed in game by the operator on 2026-10-17 (which client was not recorded); if it ever differs, that pet records nothing.
  • Ticking Threat turns on what it needs. Asked by the operator on 2026-10-17: "when you check the threat to turn it on, it turns on all the other settings required for it to work if they are not on". Recount:EnableThreatRequirements, called from the Modules checkbox in GUI_Config.lua, switches on global data collection (through Recount:SetGlobalDataCollect, so the Data Collection box follows), Record Combat Only, recording for yourself, your group and pets (threat is only stored on a combatant the combat log already created), and Record Time Data for yourself, so the threat graph has a series. Record Combat Only is needed because threat is only polled in combat, and with it off Recount never enters combat, so both threat modes stayed empty (Peer Review, thread ef07ad9f, F1). The checkbox then re-reads every settings box from the profile (me:LoadConfig); before that, ticking Threat with the settings open left the filter boxes showing off, and the next filter click wrote those states back and undid the requirements (F2). Known cost: Recount has no setting that turns Record Combat Only off (its only writes are the default in Recount.lua and this one), so the one way to lose it after ticking Threat is a saved profile that already has it off, from an older version or a profile switch; threat then records nothing until Threat is ticked again. It does not touch the zone and group-size filters, which can still stop collection where you are.

Changed

  • The Damage and Healing tooltips count every pet, not only the top one. With Merge Pets on, both tooltips picked the single pet with the most damage or healing, so a shaman with several totems saw one. The pet line now gives every pet's combined share, naming the top pet and how many others there are (Searing Totem <Shaman> +1 (12%)), and the two pet Top 3 lists draw from all of them, each row labelled with its pet when there is more than one. One shared addPetTooltip in Recount_Modes.lua replaces the two copies.
  • Power gains out of combat are dropped when Record Combat Only is on. Recount:AddGain recorded every energize and leech with no combat check, so Mana Spring ticks and regen procs between pulls went into Overall and into the next fight. It now returns as healing does. Unlike Dispels it does not start a fight, since a fight per Mana Spring tick would fill the fight list with empty fights. As in AddHealData, both sides are stamped active (SetActive) before the return. My first version returned before the stamps, so a giver whose only activity was mana between pulls could be deleted 30 seconds later by the idle sweep in Recount:TimeTick, earlier fights included. Found by Peer Review (thread eac2bef3, R1).
  • Threat bars show real threat as ThreatClassic2 does, and a click opens the threat graph. Asked by the operator on 2026-10-17: "it's also not showing the threat valiue, 3 and 4 isn't useful. it should look like threatclassic2", and "i can't click into the rows to see what the threat is ... like clicking into dps brings up the graphs, we should have the same thing for threat". Live Threat now keeps the threat itself (the client's value divided by THREAT_SCALE) instead of thousands to one decimal, and its returner gives the client's share of aggro as a second value. Recount.LiveModes tells me:RenderPane and Recount:ReportData in GUI_Main.lua that this second value is a share, not a rate: it is never added to the total's per-second figure, and each bar and report line reads the threat followed by the share, as (80%), whatever the per-second and percent bar-text options say. Threat's bar is its saved peak times 1,000, rounded, and both tooltips use Recount:FormatLongNums. Peaks stay saved in thousands but are now kept to the single point (KThreat divides to 1/1000, not 1/10), so the Threat bar shows the same 6,294 Live Threat does instead of 6,300 (Peer Review, thread 2af65df6, P3). A Classic fight saved before the scale fix (below) held tenths of a value 100 times high, so 629.4 is exactly 6,294; RescaleSavedThreat rounded its quotient to tenths and showed 6,300, and now rounds to thousandths, so saved Classic peaks keep their exact value too (my self-audit, thread eaa2b2be, S1; confirmed by Peer Review). RescaleSavedThreat returns at once on a THREAT_SCALE 1 client (retail and Forever), so no saved peak there is rescaled; one saved there before this update was kept to tenths and still shows the nearest hundred (Peer Review, thread 2288371d, A4). The share of aggro is rounded to a whole percent where it is read, since the client's percentage is fractional and every place that prints it uses %d (P2). A chat report of Live Threat names the mob in its header (Live Threat: Ragnaros) instead of a fight, and sends no total line, since everyone's threat on one mob added up is no threat meter's figure (P4); the header code for both chat and Battle.net whispers is now one block. The mob's name stays inside the Threat module, cleared when a fight starts, and reaches the report through the new Recount.LiveModeSubjects, keyed by mode like Recount.LiveModes, so a second live mode would name its own subject instead of the threat target (thread eaa2b2be, S3). A mode's sixth element was not used for this because it also sets the window title, which is only refreshed when the mode changes. The main window's Total bar is not drawn for a live mode either, so the window and the report agree (S2). me:LayoutPane still reserves its slot, as it does for any fight with no total, so the first bar sits one row down. The threat graph is each player's threat against their own target, not necessarily the Live list's mob, since that is the only series recorded (A3). Neither threat mode has a breakdown, so a plain click on either opened nothing; the new Recount.GraphClickModes makes me:MainWindowSelectPlayer open the threat graph instead, as Shift+click already did, and the bar's right-click menu drops its "Show Details (Left Click)" entry for those modes (found on this session's self-audit). Ticking Threat now also turns on Record Time Data for pets, so a pet's bar has a graph. The share is the client's UnitDetailedThreatSituation percentage, shown whole (%d); that it matches ThreatClassic2's number in play is not verified. Not yet verified in game.
  • The title-bar mode menu is grouped into submenus, each sorted alphabetically. Asked by the operator on 2026-10-17: "lets sort this alphabetically, so it's easier to navigate", then "can we make it a multi tiered menu ... with a few main categories then sub". Recount:OpenModeDropDown in GUI_Main.lua now opens Damage, Healing, Threat, Power, Utility and Misc as LibDropdown submenus (nested type = "group" entries), in that order, from one ModeGroups table. A mode is filed by its English locale key, so the grouping holds in every language, and a mode no group names, such as one another addon registers, goes under Misc. Inside each submenu the modes are sorted by their shown label, case-insensitively, with an explicit order rather than the library's name fallback. A group with nothing in it, as Threat is with its module off, is left out. The groups are two levels deep; Power is the longest at nine entries. New enUS keys Power and Utility; Misc was already translated. In game the operator found a group that highlighted on hover and showed no submenu: FadeMenu fades a picked-from menu to alpha 0 with UIFrameFade and then releases its frames to LibDropdown's pool, and AcquireFrame handed a faded frame to the next submenu without resetting its alpha (Recount resets only the root's), so that submenu opened fully transparent. A frame reused while its fade was still running would also be released by the fade's finishedFunc from under its new menu. AcquireFrame in libs/LibDropDown-1.0/LibDropdown-1.0.lua now cancels any fade on the frame (UIFrameFadeRemoveFrame, which Classic Era and retail both define in FrameUtil.lua) and sets its alpha to 1; the library's MINOR goes from 3 to 4 so this copy wins over an older one another addon loads. Two new specs in interactions_spec.lua hover every group through the real buttons after picking a mode and letting the fade finish, and after reopening mid-fade; both fail with the fix taken out. They need GameTooltip_SetDefaultAnchor, which the harness supplies since pin 0714030 (asked for on inbox thread fcf6a420); until then Tests/env_recount.lua carried a stand-in, now deleted. The two specs run under Classic Era only; they were not run with the Mainline tooltip anchor, which differs, though nothing in Recount or LibDropdown reads where the tooltip was anchored. The submenu fix is not yet verified in game.
  • Removed: Recount:BarDropDownOpen and Recount.CreateBarDropdown. The older UIDropDownMenu bar menu, replaced long ago by the LibDropdown menu Recount:OpenBarDropDown opens, had no caller in Recount, and none of the addons installed under Classic Era call either method (Peer Review's search, thread 4dd1717d); other flavours' AddOns trees were not searched. A plugin that still calls one of them now raises. The two unused UIDropDownMenu_AddButton and UIDropDownMenu_SetAnchor locals went with them.
  • GUI_Main.lua: one menu placement, and dead code removed. The bar, fight and mode menus each carried a copy of the "open beside the frame, on the side with more room" block, and a fourth sat in the removed BarDropDownOpen; they are one PlaceMenuBeside, pinned by a placement spec per menu in interactions_spec.lua and guimain_spec.lua. Removed because nothing could reach them, each by reading and each now pinned by a spec that drives the code around it: me:CreateFightDropdown and the commented-out FightDropDownOpen, ModeDropDownOpen and CreateModeDropdown (no caller); Recount.ShortNumber's duplicate T, G, M and k branches; me:SetBar's row-range guard (its two callers in me:RenderPane only pass row 0 when the total bar is shown, which is when row 0 exists); me:BindPane's nil-mode check (an unknown tab label falls back to mode 1, and the mode list always starts with Damage Done); Recount:ShortModeName's check for a true locale value (AceLocale-3.0 stores true as the key, AceLocale-3.0.lua:50 and :65; a newer AceLocale could change that); and SetTabLevel's no-tab-strip guard (only called on the main window once the strip exists). me:AddCombatantToGraphData is folded into Recount:AddAllToGraph (below). New Tests/guimain_spec.lua (75 examples) takes GUI_Main.lua to 100% line coverage (Peer Review, thread f4093173, S4).
  • With Merge Absorbs on, the Healing tooltip's pet share now counts the owner's absorbs. The bar adds them (MergedPetHealingDPS) but addPetTooltip's denominator did not, so the pet's percentage read high. Found by Peer Review (thread eac2bef3, N1).

Fixed

  • Threat read 100 times too high on every Classic client. Reported by the operator on 2026-10-17 ("our threat numbers seem to be off") against ThreatClassic2. Classic clients return UnitDetailedThreatSituation's threat value at 100 per point of damage; the retail client, which Forever runs, at 1. ThreatClassic2 divides by 100 on every client but those two, behind its downscaleThreat option, which defaults on and is described as "downscale threat so 1 damage = 1 threat" (dfherr/ThreatClassic2 core/core.lua UpdateThreatData and core/config.lua, read 2026-10-17). TrackerModule_Threat.lua divided only by 1,000 for its k display, so 6,294 threat on Classic Era showed as 629.4k, and the TPS graph was 100 times high too. It now divides by THREAT_SCALE: 1 where WOW_PROJECT_ID == WOW_PROJECT_MAINLINE or Recount.IsMidnightClient, 100 elsewhere. The retail and Forever scale rests on ThreatClassic2's code, not on a Blizzard document (not verified in game). Fights saved before the fix are divided down once when the saved data loads (a wrap of Recount.LocalizeCombatants), peaks and TPS series both, on Classic clients only. Each combatant is marked ThreatScaled once rescaled or once the poller records for it, so a value on the new scale is never divided again. The mark holds the divisor (100 on Classic, 1 elsewhere) rather than true, so a later correction to the scale can tell what saved values were divided by (Peer Review, thread ef07ad9f, F4). MoveFights points LastFightData and Fight1 at one table, so the rescale divides each table once however many keys reach it; otherwise that fight would end up 10,000 times low if saved data keeps the sharing across a reload, which is not verified (F3).
  • A healer with a friendly target recorded no threat. The poller read each member's threat against their own target only, and skipped them when it was not hostile, so a healer targeting the tank never appeared in Threat. It now falls back to the target's target, the same targettarget ThreatClassic2 uses for a friendly target. Each member is still read against their own target, where ThreatClassic2 reads everyone against the player's target, so a player on a different mob shows their threat on that mob; the new Live Threat mode (above) is the ThreatClassic2-style view.
  • A bar whose value fell to zero stayed on the main window at its old value. me:RenderPane in GUI_Main.lua dropped the combatant from its lookup but looked for the bar by comparing each row's name with the combatant's record, which never matched, so the bar stayed until the mode or fight changed. Live Threat shows it whenever someone's threat on your target goes; whether any other mode reached it in play is not traced. Rows are now matched by identity, since the sorted table holds the lookup's own row tables. Recount:ReportData, which adds to the same tables, likewise never removed a row whose value had fallen since the last redraw, so a chat report could send it; it now drops every row it did not refresh. Because the report writes the new values into the window's rows, the next redraw saw nothing changed and skipped setting the bar scale, so after a report the bars stayed scaled to the old top value until some value moved; ReportData now sets MaxValue from the top row it sorted, through one sortRows helper that me:RenderPane shares so the two cannot drift apart (Peer Review, thread 28e0c74f, B2). Rows from a previous mode or fight cannot reach a report: Recount:SetMainWindowMode and every fight-menu choice empty both tables first. Found answering Peer Review's question on the self-audit (thread 36c733ea).
  • The Threat bar menu offered a realtime graph that never moved. The Threat mode gave realtime settings ({"Threat", "'s Threat (k)"}), so its bar menu listed "Show Realtime Graph (Ctrl Click)" and a Ctrl+click opened a window registered on Tracking["Threat"]. Nothing ever calls that entry: only the combat-log handlers feed realtime stats, for damage, damage taken, healing and healing taken, plus five timer stats. The window stayed flat for the whole fight. The mode now has no realtime settings, so the entry is gone and a Ctrl+click opens the threat graph like a plain click. Found by Peer Review (thread 2af65df6, P1).
  • A shaman from another realm got no credit for Damage Prevented. TrackerModule_Mitigation.lua read the Stoneskin caster with UnitName, which returns the realm separately, while combatants are keyed by the combat log's Name-Realm. The lookup now joins them through the new Recount:UnitCombatantName in roster.lua, which the threat module and Recount:FindOwnerPetFromGUID use too, in place of three spellings of the same join (Peer Review, thread ef07ad9f, F5). It joins only a non-empty realm and never on Forever, where UnitName's second return is a surname. Forever is tested as IsMidnightClient and not IsRetailGame: my first version skipped on IsMidnightClient alone, which is also true on retail 12.0+, where the second return is a realm, so it would have stopped joining realms there (found on this session's self-audit). FindOwnerPetFromGUID had joined any second return, so an empty realm gave the owner Name- and, on Forever, a party or raid pet's owner had their surname joined to their name; either way the pet was filed under an owner who does not exist. That Forever's second return is a surname is Blizzard's own reading: Forever's NameUtil.FormatUnitNameForDisplay takes local name, surname = UnitName(unit) and joins the two with CHARACTERNAME_SURNAME_SEPARATOR (wow-ui-source-forever at bd2470a, Blizzard_FrameXMLUtil/Camelot/NameUtil.lua; found by Peer Review, thread a4876f8a). The same file says that with RegionalUniqueNamesEnabled() the first return can already hold name and surname together; whether that bare first return matches the combat log's name on Forever is not verified. It is also not reachable in play as far as the README says: on Forever, Damage Prevented and threat have no data source and stay blank, and FindOwnerPetFromGUID is reached only from Recount:DetectPet on the combat-log path, so the Forever half of this entry is a correctness fix with no visible effect today (that DetectPet never runs on Forever is not traced). Known cost: pets already saved under a wrong owner name (Name- or a surname-joined owner) stay under it; nothing migrates them, and they go with Reset Data, which deletes every combatant.
  • Deleting a combatant left their guardians mapped to them. Recount:DeleteGuardianOwnerByGUID indexed GuardianReverseGUIDs with the record it had just read from it, which matched nothing, so it never removed a GUID. It now walks each record's GUIDs, with pairs because the first GUID is stored at index 0.
  • Mana Given and Dispels could raise for a combatant with no row in the chosen fight. Both check only that the combatant exists, and Recount:MergedPetAmount and Recount.MergedPetBreakdown indexed the fight row unguarded. Both helpers now read a missing row as empty. Found by Peer Review (thread 71c1a081).
  • Fifteen modes could raise for a combatant with no row in the chosen fight, and so could the Activity tooltip. The main window lists combatants, not combatants with data for the selected fight, and these returners checked only that the combatant existed before indexing data.Fights[CurDataSet]: Dispelled, Interrupts, Interrupted, Ressers, CC Breakers, Damage Prevented, Totems and the eight power-gain modes (Mana Given was already guarded). Each now reads zero. Every returner and tooltip in Recount_Modes.lua and the seven modules now gets the row from one Recount:CurrentFight(data), which returns nil when there is no combatant, no Fights table or no row for the selected fight, in place of the two-part guard copied into about 25 functions, so a new mode cannot be written with half of it (Peer Review, thread dca25a1a, F3). The main window's bar and report loops in GUI_Main.lua use it too; two of their three copies skipped the Fights check. The detail window's summary page (Recount:UpdateSummaryMode) read the row unguarded and raised for a combatant with no row in the chosen fight; it now shows zeros. TooltipFuncs:ActiveTime read TimeHeal and TimeDamage off the same unguarded row. Found by the new modes_spec sweep below, which raised for each.
  • The graph window's "add all" showed only one line when the graph was closed. Recount:AddAllToGraph called me:AddCombatantToGraphData once per listed combatant, and each call restarted the comparison when the graph window was hidden. The window is only shown by the SetGraphData after the loop, so every combatant restarted it and only the last one was plotted. AddAllToGraph now decides once whether to start a new comparison (none running, graph hidden, or a different mode) and then adds everyone; the helper is gone. It still adds the main window's mode and rows, where an Alt+click follows the pane that was clicked, since the graph window's button has no pane of its own; so a comparison started from the lower pane or the tab window in another mode is replaced, not added to (Peer Review, thread dd45be6d, X1; pinned by a spec). The existing interactions_spec example checked only the graph's title, so it never saw this; guimain_spec's "adds each listed combatant" failed on the old code (desk run 20261018-083332.940) and passes now. Found writing that spec (thread 4dd1717d, A1).
  • "Add all" to the graph raised for a combatant with no time data. TimeData is created only once time data is recorded, and the Time Data filter is off by default for grouped and ungrouped players, so a groupmate can have none. The restart branch indexed it unguarded, while the other two graph paths checked it. Raised without the check (desk run 20261018-083415.222), passes with it. Found by Peer Review (thread ffbf1592).
  • Dead branches removed. AddSortedTooltipData's percent = 100 for a zero total could not run, because total starts at Epsilon. SpellAuraDispelledStolen's SPELL_DISPEL_FAILED check could not run, because the dispatch table in Tracker.lua sends that event to SpellAuraDispelFailed. ThreatReturner had two identical returns. The mitigation module's roster handler checked UnitGUID before calling rescanUnit, which checks it (and secrecy) itself; the handler now leaves that to rescanUnit.
  • A normal melee swing could be labelled as an extra attack. After SPELL_EXTRA_ATTACKS (Windfury, Sword Specialization) Tracker.lua takes the next damage event as the trigger and labels the next Melee "Melee (Windfury Totem)" or "Hamstring (Windfury Totem)", for up to 5 seconds. Published Classic combat logs (magey/classic-warrior #7, magey/forever-warrior #28) show the proc event comes before the trigger's damage and the extra swing within ~200ms, which that order handles. But a Slam's damage lands after the extra swing, so the extra swing was taken as the trigger and the normal swing 2-3 seconds later took the label. The window is now 1 second (EXTRA_ATTACK_WINDOW), and a new proc replaces a pending one older than that instead of being ignored behind it. The first Melee inside the window is now always the labelled swing: when it lands before any trigger damage (Slam) it is labelled "Melee (Windfury Totem)" instead of being taken as the trigger and leaving the proc unlabelled (Peer Review, thread dca25a1a, W1). The cost: after a proc off a white swing whose damage is logged after the proc, the trigger swing takes the label rather than the extra one; both are Melee, so the count and name are right but the label sits on the other swing's damage. The order for a proc off a plain white swing is not in either log. Pending procs are now keyed by GUID rather than name, because the damage path looks them up after renaming a pet to Name <Owner>, so a pet's proc was never labelled (F1). Not yet verified in game.
  • A mode registered twice was listed twice. Recount:AddModeTooltip appended unconditionally; it now replaces an entry with the same name, keeping its place in the list. This also means another addon that registers a mode under a name Recount already uses now replaces Recount's mode instead of listing beside it (thread 6048156e, A3). Found by the duplicate-listing check added to the revision specs after Peer Review's F5 (thread dca25a1a). AddModeTooltip and Recount:SetupMainWindow now share one findMode lookup by label (thread 6048156e, A2): SetupMainWindow's seven copies of "find the optional mode, add it if its module is on, remove it if off" are one loop over an OptionalModes list, and a removed mode is taken out with table.remove instead of being set to nil and closed up by a second loop. Recount:GetModeIndex moved to Recount_Modes.lua and is now the one lookup by label for everything else: GUI_Main.lua's modeIndexOf (the tab bar) is gone, and Recount:SetMainWindowModeByLabel (the key bindings) calls it. It tries the label as shown, then the label as an English locale key (thread 88a80cd4, B1). The old GetModeIndex matched either in one pass, so a locale key whose translation was an earlier mode's shown label picked that mode. SetMainWindowModeByLabel read L[label], which made AceLocale report a missing entry for any label that is not a locale key; it reads with rawget now, as GetModeIndex already did.
  • The detail window's summary could raise after its combatant was deleted. Deleting a combatant from the bar menu left the detail window open on them. Clicking the summary's Pet line or its Outgoing/Incoming toggle then rebuilt the summary for MainWindow.Selected, which still named the deleted combatant, and Recount:UpdateSummaryMode read data.Pet with no record. Recount:DeleteCombatant now hides the detail window when it shows the deleted combatant, and UpdateSummaryMode returns when there is no record. Found by Peer Review (thread 88a80cd4, B3).
  • Hovering a plotted compare graph anchored its crosshair and tooltip to the wrong object. The graph's OnUpdate handler names its frame graph, but three calls still passed self, which inside Recount:CreateCompareWindow is the Recount addon object, not a frame: the two hairline:SetPoint anchors and GameTooltip:SetOwner. They now pass graph. What the client did with the addon object as an anchor is not verified in game. GetCursorPosition is now cached as a local.
  • The compare graph window was English only. Every label, button, dropdown entry, status line and tooltip in GUI_CompareGraph.lua was a literal; all 43 now go through the locale, with new enUS keys, so locale_spec checks them. Numbers in text use format keys (Fight %d (%ds), Showing %d series). Found by Peer Review (thread 88a80cd4, N2), which named the Fight filter's four.
  • The Titan Panel button had its own copy of the fight-row guard. It now reads the row through Recount:CurrentFight. Its or "OverallData" fallback is gone; CurDataSet is set at load. Found by Peer Review (thread 88a80cd4, N1).
  • "Top 3" tooltips listed one row three times. Recount:AddSortedTooltipData looped for rank = 1, num but read SortedData[i], where i is the count of rows collected, so every line showed the last-sorted entry with numbers 1, 2 and 3. It now reads SortedData[rank]. Every mode's tooltip goes through it.
  • A grouped pet or totem whose owner was never seen lost all its numbers. Recount:AddCombatant gave a grouped non-player with no known owner no type (the branch was commented out), and AddAmount drops everything for a nil type. The common case is a party member's totem already standing at a /reload, whose summon Recount missed and which has no unit token to find the owner by. It is now typed Unknown, which is recorded by default and hidden from the bars, so its data is kept though it cannot be tied to a shaman.
  • English players saw "Maelstorm" for Maelstrom. The mode name, its key binding's name, the detail titles and the tooltip's Top 3 headings came from enUS keys spelled Maelstorm, with the value true. The keys stay as they are, since every locale and Bindings.xml use them; the five enUS entries now have values spelled Maelstrom. GetModeIndex falls back to the locale key, so the binding still selects the mode, and the short tab name Mael follows because SHORT_MODE_NAMES is keyed by locale key. Saved tabs hold the shown label, so a tab saved as "Maelstorm Gained" would have shown the initials fallback MG (Mana Gained's short name) until clicked; Recount:LoadMainWindowData now renames every saved tab whose label resolves to a mode to that mode's shown label, which also renames a tab saved with an English label when the addon is later run in another language (not the reverse, since only English labels are locale keys). Found on the self-audit (thread d149f829, S4); the saved-tab half was found on this session's own audit.

Documentation

  • README, CurseForge description and docs/FEATURE_IMPROVEMENTS.md describe the Totems mode and that a totem's mana, dispels and overhealing now count for its shaman with Merge Pets on. Both player docs list Totems among the always-available tracker modes, among the modes with no keybinding (Bindings.xml has none), and among the modes that stay blank on Retail Midnight and Forever. The CurseForge Recent Updates gains v1.20.0 and drops v1.19.2, keeping five. Both player docs now describe the grouped mode menu and the graph window's + (add everyone) button, and docs/FEATURE_IMPROVEMENTS.md records the grouped menu.

Testing

  • modes_spec: Totems. The shared fixture now has Healbot as a shaman dropping two Searing Totems and a Windfury Totem, so the per-mode sweep covers Totems; value specs check the count, the per-totem breakdown, the bar and detail, and that a non-shaman's summon, Feral Spirit, a totem's own summon and a caster with no flags are all left out. With Record Combat Only on: nothing is recorded before a fight, a totem 30 seconds before the pull counts in that fight, one 121 seconds before never counts, a held totem counts once however many fights follow, a stale held totem is dropped from the list (read through the spec-only Recount:TotemsHeldCount), recent totems still count after stale ones are dropped, and a reset forgets held totems. The prune and reset specs were each seen to fail with their fix removed. TrackerModule_Totems.lua at 100% line coverage; 933 passed.
  • modes_spec: totems credited to their shaman like pets, and Top 3 tooltip lists. A totem's mana given, dispels and overheal count in its shaman's Mana Given, Dispels, Overhealing Done and Healing detail with Merge Pets on and are left out with it off; the Top 3 tooltip lists three distinct rows in rank order. 940 passed. Every changed line in Recount_Modes.lua, TrackerModule_PowerGains.lua and TrackerModule_Dispels.lua is covered (desk coverage run 20261017-081521.772-3076417322); those files as a whole are at 87.8%, 96.1% and 90.7%, the rest being older guard returns and pet tooltip branches.
  • modes_spec and mitigation_spec: the totem audit leftovers. A totem's mana out of combat with Record Combat Only on changes nothing and starts no fight; the Healing tooltip lists two totems as "+1" with both named, and a lone pet by name with no count; a combatant with no row for the fight reads as zero and an empty breakdown; deleting an owner forgets every guardian GUID, the first included; a cross-realm shaman's Stoneskin credits Name-Realm. combatlog_spec's idle-sweep spec turns Record Combat Only off, since its ticks leave combat and it is about the sweep, not the gate. None was run against the old code to watch it fail. 946 passed, 0 failed (desk run 20261017-084224.337-661286822). Under the coverage runner the threat spec "clears its samples when a new fight starts" fails (6 samples, expected 5) with any set of targets, while it passes in the plain run and alone; the cause is not established and it is on the todo list.
  • combatlog_spec: extra attacks. The label lands on the extra swing after an instant trigger; a normal swing 2.5 seconds after a Slam-ordered proc stays plain Melee; a fresh proc replaces one 3 seconds old that never saw its swing. By reading, not by running: the second fails on the old 5-second window; the third passes on the old code and fails only if the window shrinks without the stale-proc replacement. 949 passed, 0 failed (desk run 20261017-085721.316-3048413046).
  • combatlog_spec, modes_spec, mitigation_spec and env_recount: the answers to thread eac2bef3, and 100% coverage. A giver out of combat with Record Combat Only on is stamped active but records nothing, and survives the idle sweep; it failed with the gate moved back above the stamps. A second idle-sweep spec runs with Record Combat Only on and combat held across the ticks. env_recount's core load is now wrapped in the harness's retainDuring, and each reset calls clearLoaded() before reinstalling globals; retainDuring alone kept every load's frames alive and tripped the 12,000 live-frame guard in every spec. With both, the threat spec "clears its samples when a new fight starts" passes under the coverage runner, which is two runs, not an established cause. modes_spec adds a sweep of every mode for a combatant with nothing to show (which found the fifteen crashes above), specs for the branches the shared fixture does not reach, revision tracking, the optional-mode toggles of each module, the Merge Absorbs pet share (failed with the owner's absorbs dropped from the share) and a check that the second Searing Totem was tracked before the delete spec asserts it gone. mitigation_spec adds the Stoneskin label on a client whose C_Spell has no GetSpellInfo. Recount_Modes.lua and the Dispels, PowerGains, Interrupts, CCBreakers, Resurrection, Mitigation and Totems modules are at 100% line coverage (desk coverage run 20261017-092334.638-1930624320); 1047 passed, 0 failed (desk run 20261017-092721.191-2973030455).
  • combatlog_spec and modes_spec: thread dca25a1a. The Slam-order spec now expects the extra swing labelled "Melee (Windfury Totem)". New: a swing 2.5 seconds after an unmatched proc stays plain Melee (failed with the window set back to 5 seconds); two swings after a white-swing proc label one (failed, with the Slam spec, with the label branch requiring a captured trigger as before); a pet's extra swing is labelled on the pet (failed with procs keyed by name). The revision specs now check that a module's reload lists no mode twice (failed for all six modules before the AddModeTooltip fix). 1050 passed, 0 failed (desk run 20261017-093848.205-2880153567).
  • modes_spec: one guard, and the optional-mode list. The "nothing to show" sweep adds a combatant record with no Fights table to every mode's returner and tooltip; with Recount:CurrentFight reduced to the old one-part check it failed 57 times (desk run 20261017-095003.688-2462097094). A new spec switches three optional modes off at once and checks the list has no gaps and keeps its order. interactions_spec opens the summary for a combatant with no row in the fight; it failed with the old unguarded read (desk run 20261017-095842.977-2105790810). 1053 passed, 0 failed (desk run 20261017-095718.924-3949104247). Recount_Modes.lua and the seven modules are at 100% line coverage, and the nil-GUID return in SpellExtraAttacks is covered (desk coverage run 20261017-095316.243-2918266046).
  • interactions_spec and guidepth_spec: thread 88a80cd4. On a simulated translated client, GetModeIndex and SetMainWindowModeByLabel find a mode by its English key; a shown label wins over a locale key that translates to another mode; an unknown label leaves the mode alone and reports no missing locale entry. The last two failed against the old one-pass lookup and L[label] (desk run 20261017-100744.642-1715742182). Deleting the combatant the summary shows closes the detail window, and a click on the summary's Pet line then does not raise; both failed with the two guards removed (desk run 20261017-101039.429-4089132674). Hovering a plotted compare graph shows the readout owned by the graph frame; it failed with self put back (desk run 20261017-101852.156-2856416235). 1060 passed, 0 failed (desk coverage run 20261017-102156.695-3378063772, which runs the suite). Recount_Modes.lua is at 100%. Every line changed in GUI_Main.lua, GUI_Detail.lua, GUI_TitanPanel.lua and GUI_CompareGraph.lua is covered, except the compare graph's Fight dropdown entries and Fight Filter tooltip, which no spec opens. Those files as a whole are at 81.0%, 79.4%, 95.3% and 80.7%.
  • modes_spec, guidepth_spec and interactions_spec: thread d149f829. The five Maelstrom entries read Maelstrom in English, and the old-spelling key, the binding's SetMainWindowModeByLabel call and the short name all still reach the mode. The compare graph hover spec also checks both crosshair anchors are the graph, and a click on the deleted combatant's summary Outgoing/Incoming toggle does not raise; both failed with self put back on the anchors and the summary's early return removed (desk run 20261017-103050.517-2685381676). tabs_spec renames a saved "Maelstorm Gained" tab to "Maelstrom Gained" with short name Mael and leaves a label no mode answers to alone; it failed with the rename disabled (desk run 20261017-103859.468-3846036294), and the Maelstrom specs failed with the enUS value put back to true (desk run 20261017-103927.787-100410570). 1063 passed, 0 failed (desk run 20261017-104039.426-2487061454).
  • modes_spec: totems known by spell name. The fixture names Searing, Windfury and Fire Elemental Totem through the harness's wow.spells. New: a shaman's Arcanite Dragonling is not counted; a Searing Totem cast at a later rank's id counts under the shared name; with no totem names from the client nothing is counted or held. All three, and the Feral Spirit spec, failed with the spell check removed (desk run 20261017-123221.592-1176617685). The non-shaman spec now also casts Windfury Totem, since a summon without a totem's name no longer reaches the class check. A Season of Discovery Decoy Totem counts; it failed with its id removed from the list (desk run 20261017-124439.880-501608816). TrackerModule_Totems.lua at 100% line coverage; 1067 passed, 0 failed (desk coverage run 20261017-124323.695-136428143, which runs the suite).
  • interactions_spec: pet threat, Live Threat, the rescale and the checkbox. A solo player's pet is read under Fluffy <Testchar>, the name the fixture's combat log stored it by; a party member from another realm under Name-Realm; a unit or pet owner with no name is skipped. With pets hidden and merged, Threat still gives the pet and its owner bars. Live Threat reads everyone, pet included, against your target, falls back to your target's target, is empty with no hostile target, drops someone whose threat is gone while their fight peak stays, and clears when a fight starts; its tooltip has the threat and aggro share, the threat alone when the client gives no share, and only the name with no reading. A saved peak of 629.4 and a TPS series are divided by 100 once and not again, threat recorded on the new scale is left alone, and a combatant with no fights is marked. EnableThreatRequirements turns on every setting it names. modes_spec lists Live Threat with the modes its fixture cannot feed, and tabs_spec's short-name sweep found its missing short name. A Live Threat bar's tooltip, bar menu and plain click are driven too. None of these was run against the code without the change. TrackerModule_Threat.lua at 100% line coverage; 1099 passed, 0 failed.
  • interactions_spec, config_spec and roster_spec: thread ef07ad9f. EnableThreatRequirements turns Record Combat Only back on. Ticking the Threat box with the settings open shows the filter boxes it turned on, and a Show click afterwards keeps them on. A fight two keys share is divided once. The rescale and the poller both mark with the divisor, 100. A Live Threat bar and report line show with an older fight selected that has no row for the player. UnitCombatantName joins another realm, leaves an empty realm off, never joins on Forever, still joins on retail 12.0+, and returns nothing for a nameless unit. The raid pet, party, party pet and focus branches of FindTargetedUnit and roster.lua's revision line are now driven. None of these was run against the code without the change. TrackerModule_Threat.lua and roster.lua at 100% line coverage; 1113 passed, 0 failed (desk coverage run 20261017-160941.806-949774343, which runs the suite). The plain suite run twice hit the 60-second per-file limit in modes_spec while the CPU read 100% with other test runs going; with --timeout 300 it passed.
  • interactions_spec: stale bars, stale report rows and the bar scale (threads 36c733ea, 28e0c74f). A Live Threat bar goes once its threat reads zero; a combatant zeroed since the last redraw is left out of a chat report; a report that refreshed the values leaves the bars scaled to the new top value. All three failed without their fix (desk runs 20261017-163400.745-1569536396 and 20261017-171728.449-2423574238). 1116 passed, 0 failed (desk run 20261017-171533.327-3281139599, with --timeout 240; two default runs before it stopped modes_spec at the 60-second per-file limit with the CPU at 99%).
  • interactions_spec and modes_spec: threat as ThreatClassic2 shows it, the click, and doubled bars. Live Threat returns the threat and the share; a bar reads the threat and (80%), or the threat alone when the client gives no share; a chat report line reads the same and its total has no summed shares. A plain click on a Live Threat or a Threat bar opens the graph window and no detail window. Threat's tooltip and bar show the peak as real threat. Two specs for the doubled Live Threat bars reported in game the same day: one combatant gets one bar across readings that fall to zero and come back, and across a pull that Feign Death splits into several fights; reverting the row match in me:RenderPane reproduced the doubling (taken from an earlier session's account and not re-run, so not verified). The click and bar-text specs were not run against the code without the change. 1122 passed, 0 failed (desk run 20261017-183921.639-3932648241, --timeout 240; a default run before it stopped modes_spec at the 60-second limit with the CPU at 97%).
  • interactions_spec: thread 2af65df6. A Threat or Live Threat bar's menu has no realtime entry and a Ctrl+click on one opens no realtime window; a Damage Done bar's menu still has both Details and the realtime entry, which proves the absent keys are spelled as the menu spells them. A Classic reading of 629,400 saves a 6.294 peak and the Threat bar reads 6,294. A share of 79.6% shows as 80% from the returner, on the bar and in a report line. A Live Threat report's header is Live Threat: <target> and it sends no total with the total bar on. 1124 passed, 0 failed (desk run 20261017-190650.005-12603724).
  • interactions_spec: thread eaa2b2be. New: Live Threat shows no Total row while Damage Done, with the same combatants, shows one; a Damage Done chat report still sends its total line. The rescale specs expect 6.294 rather than 6.3. Each fix was then broken on its own and its spec seen to fail: the realtime settings put back on Threat (the menu check, desk run 20261017-192049.446-2853367936); only the Ctrl+click path forced to open a realtime window (both Ctrl+click checks, desk run 20261017-192305.211-347880913, which also settles that the spec's Ctrl key reaches GUI_Main's cached IsControlKeyDown and that a realtime window is detectable); the Total-row gate, the report subject, the share rounding and the KThreat precision (one spec each, desk run 20261017-192516.338-1484307619); the report's total gate (desk run 20261017-192721.730-3531921440). TrackerModule_Threat.lua at 100% line coverage; GUI_Main.lua at 82.45%, where the uncovered lines this work touched are the Battle.net whisper branch of the report header and the report total's per-second figure (desk coverage run 20261017-193146.180-855057418). The third rescale assertion ("divides a fight two keys share only once") now checks the exact 6.294, that the result is not the old rounded 6.3, and that Fight1 still shares the one table; Writ's test-weakening guard had refused a plain 6.3 -> 6.294 swap there (thread 7d5d14fc, closed). Whole suite 1132 passed, 0 failed (desk run 20261017-225926.452-2918418133).
  • interactions_spec: the Battle.net report and the rest of the report header (threads eaa2b2be C1, 5dda0799 B3). A Damage Done report to a Battle.net friend whispers the header, a total with its per-second figure and the bars, all to that friend and none to chat; it runs twice, once through C_BattleNet.SendWhisper, the function GUI_Main.lua prefers, and once through the BNSendWhisper fallback with no C_BattleNet.SendWhisper. At harness 943bc1c the C_BattleNet.SendWhisper variant asserts on the harness's own wow.bnWhispers, and passes a BNSendWhisper that raises, so a GUI_Main that preferred the global fails it (seen to fail with line 29's order flipped). The fallback variant keeps its own BNSendWhisper, because the harness's forwards to the C_BattleNet.SendWhisper that variant hides. Every load resets the env, which rebuilds both, so nothing carries into a later load (threads 7b76cf8b, e7912e7f). It first failed with the header's whisper branch sent to chat instead (desk run 20261017-201043.560-2332766922). A Damage Taken report's total line is exactly 0. Total 300 (100%), with no rate. A report on a numbered fight is headed Recount - Damage Done for <fight name>, and with no fight name sends no header. A mode registered with its own total and report title, whose bar rate is text, reports exactly its title, 0. Total 1000 (100%) and 1. Testchar 500 (fast, 50%). Every line of Recount:ReportData is now covered; GUI_Main.lua is at 83.42% (desk coverage run 20261017-204238.927-3006797611, which ran the suite before the rescale assertion above was settled). The rest of GUI_Main.lua's uncovered lines are older than this release: the fight dropdown entries, the compare-graph bar click, the tab and divider menus, the large-number formats and similar, which no spec drives yet.

[v1.19.6] (2026-10-08) - LibDBIcon installs again, and corrected docs

Fixed

  • The Threat box on the Modules settings tab now follows the profile when the settings reopen. LoadConfig in GUI_Config.lua re-read six of the seven Enabled Modules checkboxes and skipped Threat, so after a profile change it kept showing the previous profile's state. Found by the documentation audit below.
  • The CurseForge app was told LibDBIcon-1.0 ships inside Recount, so it stopped installing it. Found by Peer Review (thread 5055212c). The copy under libs/LibDBIcon-1.0, which only the Forever TOC loads, still carried the packager's --@curseforge-project-slug: libdbicon-1-0@ line. While copying files the packager reads that line as "this library is embedded" and overwrites .pkgmeta's required-dependencies entry for the same slug, so every upload sent LibDBIcon as an embedded library. Every flavour but Forever lists LibDBIcon-1.0 in ## Dependencies and will not load without it. The line is removed; the copy stays for Forever. Not verified on CurseForge until the next upload.

Documentation

  • README and CurseForge description brought in line with the code (read-only audit of both docs against the source, 2026-10-04). Corrected:

    • the tracker-module modes (Interrupts, Dispels, CC Breakers, Ressers, Damage Prevented, the power gains, Mana Given) cannot be switched off; only the seven core modes on the Modules tab can (GUI_Config.lua CreateWindowModuleSelection).
    • /recount gui opens a separate Ace3 options window (Recount.lua, AceConfigDialog:Open), not the WoW settings page.
    • keybindings cover most modes, not all: Bindings.xml has none for Absorbs, Threat, Interrupted, Damage Prevented or Mana Given.
    • window colours are title, background and title text per window plus bar text and Total bar; there is no border colour and no font-size setting.
    • Ctrl+click live windows include Threat; Power Gains lists all eight power types; the Ressers line no longer claims combat resurrections only.
    • sync is off by default, so the troubleshooting tip now says to turn it on.
    • on retail and Forever, Deaths shows no rate.

    Added, previously undocumented: Alt+click a bar (comparison graph), Ctrl+Alt+click (remove a combatant), Alt+click reset (no prompt), /recount uilanguage, the Show Buttons / Show Total Bar / Show Scrollbar / Autohide On Combat options, Merge Absorbs w/ Heals and w/ Damage, the per-type Show / Record Data / Record Time Data / Record Deaths filter columns and the hostile, trivial and unknown rows, the scenario and arena zone filters and solo / party / raid group filters, Hide While In Pet Battle, the "only a different instance" and Delete on New Raid clearing options, Autodelete Time Data, and the broker's left-click and data-set names.

Testing

  • config_spec: every Enabled Modules box is re-read from the profile when the settings reopen, both ways (all off, then all on). Not run against the old code to watch it fail; the old LoadConfig never touched the Threat box, which the assertion names.
  • toc_spec: no tracked .lua under libs/ carries an embed marker for a slug .pkgmeta requires. Run with the marker put back: it failed naming libs/LibDBIcon-1.0/LibDBIcon-1.0.lua and libdbicon-1-0; passes without it (911 passed).

[v1.19.5] (2026-10-04) - Whole-number DPS on retail and Forever

Fixed

  • Retail and Forever: an in-combat per-second rate printed every digit. Reported from Discord on 2026-10-04 from WoW Forever: the DPS "goes from normal values like 75.3 dps to 72.8595849583423". In combat amountPerSecond is secret, and v1.19.4's perSecondText handed a secret to AbbreviateNumbers unrounded, which returns anything under 1000 as it is (the plain path was already rounded). Addon code cannot round a secret, so perSecondArg in Tracker_Midnight.lua now returns a format spec with the value: a plain rate keeps "%s" with the rounded, abbreviated string, and a secret rate gets "%.0f", so FontString:SetFormattedText does the rounding (SimpleFontStringAPIDocumentation.lua declares it AllowedWhenTainted in the forever tree too). Both bar layouts that show a rate use it: DPS mode, and "total (per second)". Known cost: a secret rate of 1000 or more reads 5235 rather than 5.2K until combat ends, because deciding whether to abbreviate is a comparison on the secret. Not yet seen in game.
  • The Bar Text Options preview showed a rate with a decimal no bar ever shows. Seen by the operator on Forever 2026-10-04: the preview read "10537815 (397821.2, 100.0%)" beside bars reading "145 (36, 100.0%)". RefreshTestBar in GUI_Config.lua appended the literal string "397821.2", skipping the number format the total beside it already used. It now goes through FormatLongNums too. The sample figures themselves were a million-DPS bar, which nobody does, so at the operator's request they are now 71460 damage at 397 DPS (a three-minute fight): "71460 (397, 100.0%)" in Standard and "71.5k (397, 100.0%)" in Short. Every flavour shares this preview.

Testing

  • config_spec: the preview's rate follows the number format, picked by firing the Standard and then the Short checkbox's OnClick. It failed first with the default Short format, which is how the test came to choose Standard explicitly; it was not run against the old literal.

  • midnight_spec: a secret rate reaches SetFormattedText as the secret itself, under a %.0f spec, in DPS mode and in Damage Done with Per Second on. The harness models a secret SetFormattedText as an opaque secret result, so the spec checks the call, not the rendered text. Not run against the old code to watch it fail; the old code passed a string under %s (and in DPS mode without a percent called SetText), which both assertions reject.

[v1.19.4] (2026-10-03) - Bar text options on retail and Forever

Fixed

  • Retail and Forever: the main window's bars ignored the bar-text options. Reported 2026-10-03 from a German client, confirmed by the player to be retail: the bars read "4.1 Mio." with no rank and no "(per second)", while the bar-text preview in the config showed both. It was not a language problem: Tracker_Midnight.lua's renderRow wrote only AbbreviateNumbers(amount) and the bare name on every restricted client. It now follows Rank Number ("%d. %s", numbered by position in the whole list, so scrolling keeps the numbers right) and Per Second ("<total> (<per second>)", except in DPS mode, which already shows the rate, and in Deaths, which has none). Both go through FontString:SetFormattedText, which SimpleFontStringAPIDocumentation.lua (live tree) declares SecretArguments = "AllowedWhenTainted", so the values are formatted by the client and Lua still never operates on a secret. A plain rate is rounded first (perSecondText), since AbbreviateNumbers leaves anything under 1000 unrounded. Percent is shown only while the source's amount and the session's totalAmount are both readable (plainPercent, gated on issecretvalue, the rule GUI_Detail_Midnight's percentages already follow), and is left off when either is secret. Searched for a way round that and found none: the curve objects that map a value through points (AddPoint, Evaluate) take secret arguments only from untainted code (LuaCurveObjectAPIDocumentation.lua, AllowedWhenUntainted), and Blizzard's own meter divides in plain Lua (DamageMeterEntry.lua GetPercentageValue), which only its untainted code may do. Seen working by the operator on WoW Forever on 2026-10-03, out of combat after a fight: "1. Quebeldorf Flamehammer 137 (27, 100.0%)". Not yet seen on retail, and not yet seen during combat, where the percent is expected to drop off. Server Name still does not apply there: removing the realm is a string operation on a name that may be secret. The Forever sighting was out of combat, where the percent proves the values were readable; that SetFormattedText accepts a secret value in combat is still from the documentation, not seen in game.
  • deletion.lua: the scenario fallback in DetectInstanceChange no longer overwrites zone, and its debug line cannot raise. C_Scenario.IsInScenario returns one boolean (every caller in wow-ui-source-classic_era reads only that; the generated API docs do not list it), so inInstance, zone = C_Scenario.IsInScenario() always set zone to nil, and (inInstance or "nil") .. " : " raises "attempt to concatenate a boolean" whenever the player is in a scenario. It now assigns inInstance only and prints both through tostring. The branch runs only when GetInstanceInfo returns no zone name, so no player is known to have hit it. Found from a language-server cast-local-type warning.

Documentation

  • The README and the CurseForge description list every slash command. Five that exist in Recount.consoleOptions were missing from both: clamptoscreen, framestrata, maxfights, sync and verchk. Both now say commands are not case-sensitive (AceConfigCmd-3.0 compares lowercased, spaces as underscores) and that some command words are translated in other client languages: the keys of gui, reset, verChk, show, pause and sync are locale strings (French show is afficher, Korean reset is 초기화).
  • Both describe the bar text options (rank, realm, per second, percent, number style, colour swap), which neither mentioned, with the Short style's real output (4.10M, from Recount.ShortNumber).
  • The retail Midnight and Forever sections no longer say there are no percentages. They say the rank and rate always show, the percent shows while the numbers are readable, and the realm-name and number-style settings do not apply there.

Development

  • Language-server warnings cleared in the bundled libraries. LibBossIDs-1.0 listed four mob IDs twice (17808, 54432, 38113, 24239), each time as true; the later copy of each is removed, so the table is unchanged. LibGraph-2.0's AddDataSeries and AddFilledDataSeries use # instead of table.getn, which the Lua 5.1 manual names as the same operation ("corresponds to the new length operator"). LibDBIcon-1.0 (bundled for Forever only) carries ---@diagnostic disable: inject-field rather than a code change, so it stays upstream's code. .luarc.json declares GetMouseFocus, which LibDropDown reads with a GetMouseFoci fallback.

Testing

  • Six existing bar-name assertions now expect the rank prefix (forever_spec 72, midnight_spec 279 and 884, split_spec 495, 496 and 505): they asserted the bare name, which encoded the ignored Rank Number option. Changed at the operator's instruction. All six are on the retail/Forever damage-meter path; no Classic assertion changed.
  • midnight_spec: the bar-text options on the damage-meter renderer. Total with its per-second rate, total alone with Per Second off, no rank with Rank Number off, rank by list position when scrolled, a sub-1000 rate rounded, and no rate for Deaths. Plus the share of the session total: with the rate, alone, after the rate in DPS mode, off with Percent off, and left off for a real secret session total and for a zero one. The harness's FontString:SetFormattedText accepts a secret argument (requested on the harness channel, delivered as f1b2743). The pin moved from b1f9386 to e17d14c, which includes it. A first move to 181539b failed 647 of 745 examples on the env's live-frame guard (12,925 frames, limit 12,000): the harness's hooksecurefunc bookkeeping from 515873b kept every earlier copy of Recount alive through TrackerModule_Threat.lua's hook on the addon table. Reported to the harness, which confirmed it by measurement and fixed it in a4ca42c. The stand-in staged in env_recount.lua in the meantime is deleted.
  • deletion_spec: the scenario fallback with no zone name known prints true : nil and still wipes on instance entry. It was not run against the old line to watch it fail.

[v1.19.3] (2026-10-02) - A tab in its own window, and retail error fixes

New

  • Show in Window: a tab's display in a window of its own. Right-click a tab and choose Show in Window; Close Window on the same menu, or the window's own close button, closes it. Asked for by a player on 2026-10-01 who wanted the split view's second display disconnected from the main window. The window (Recount_TabWindow, built by me:CreateTabWindow in GUI_Main.lua the first time it is opened) is only bars and a title, moved and sized on its own, with the main window's colours (Widgets.lua now styles it as one), scale, lock, strata and clamp. It is a third pane, Recount.MainWindow.Window, with a Host field naming its window: PlaceRow, CreateRow, LayoutPane, RenderPane and the Midnight renderer now size and parent a pane's bars through its host, and me:BindPane (formerly the lower pane's BindLowerPane) binds either pane to its tab. It refreshes on its own one-second timer, so it keeps updating while the main window is hidden; RefreshMainWindow and the Midnight renderer now draw it whether or not the main window is shown. Saved per profile in MainWindow.Tabs.window (a tab index, kept right when tabs close or move; closing that tab closes the window) and .windowPos. It reopens at login (Recount:RestoreTabWindow, run after InitOrder so it joins the window stacking order) and follows a profile change. Hiding the whole UI (Alt+Z) or a pet battle does not close it. The same tab can be in the window and below at once. The main window's resize grips and position save/restore now go through shared helpers (me:AddResizeGrips, me:FramePosition, me:PlaceFrame) used by both windows. The Show Scrollbar setting covers its scrollbar (Recount_TabWindow_ScrollBar) as well. Seen working by the operator on 2026-10-02 on Classic (which Classic client was not stated); not yet run on retail or Forever.

Fixed

  • ADDON_ACTION_FORBIDDEN: Recount tried to call Frame:RegisterEvent() from OnEnable (Recount.lua, RegisterCombatLogEvent). Reported 2026-10-01 with no flavour named; the stack shows the restricted-client gate came out false, so COMBAT_LOG_EVENT_UNFILTERED was registered on a client that forbids it. The gate tested only WOW_PROJECT_ID == WOW_PROJECT_MAINLINE plus the interface number, and v1.19.0's statement that Forever reports WOW_PROJECT_MAINLINE was never verified: WOW_PROJECT_ID is engine-set there and nothing in wow-ui-source-forever assigns it for the _Camelot client. The gate now also treats a client with no combat-log row reader as restricted: every Classic tree's C_CombatLog documents GetCurrentEventInfo, the live and Forever trees' do not, and their deprecated CombatLogGetCurrentEventInfo forwards to that nil. Classic flavours are unchanged (they all have the reader). Which client the report came from is not confirmed, and the fix is not yet run in a client.
  • Retail: "attempted to perform indexed assignment on a table that cannot be indexed with secret keys" at TrackerModule_Mitigation.lua:250, 128 times in one session (reported 2026-10-01). UnitGUID itself can return a secret on 12.0+, and the module used the GUID as a key into active and guidToUnit with only a nil check; the aura fields were guarded, the GUID was not. MitigationUnitAura, rescanUnit and MitigationRosterUpdate now skip a unit whose GUID is secret (isPlain). Nothing is lost: on those clients the combat log is gone, so CreditMitigation is never reached. Not yet run in a client.
  • Retail: "GetAuraDataByIndex(): Auras cannot be accessed when secret while tainted by 'Recount'" from the Mitigation module's rescan (reported 2026-10-01). The client refuses the call itself, so no guard on the result could help. The module's event frame now registers UNIT_AURA, GROUP_ROSTER_UPDATE and PLAYER_ENTERING_WORLD only when Recount.IsMidnightClient is false: every AddDamageData caller is a combat-log handler in Tracker.lua, so on the restricted client Damage Prevented had nothing to credit. 11.x retail and every Classic flavour keep it. Not yet run in a client.

Documentation

  • The README and the CurseForge description cover Show in Window, in the window instructions and under Tabs in the settings, and say the second window is saved with the profile.
  • Both now say the tracker-module modes stay blank on retail Midnight and Forever. Interrupts, Interrupted, Dispels, Dispelled, CC Breakers, Ressers, Damage Prevented, the power-gain modes and Mana Given are combat-log trackers, and Tracker_Midnight.lua's MODE_METRIC maps only Damage Done, DPS, Damage Taken, Healing Done, Absorbs and Deaths to the damage meter, so every other mode renders blank there. The "stays blank" lists named only the built-in modes.

Development

  • .pkgmeta carries no comments. The header, the commented-out externals: example and the ignore-rule notes are removed at the operator's request; every setting is unchanged.

Testing

  • forever_spec: a client with no combat-log reader stays off the combat log, on an 11.x build the version test alone would have sent to it.
  • mitigation_spec: a unit whose GUID is secret is neither tracked nor put in the roster map. The harness cannot raise on a secret used as a table key (Lua 5.1 consults the table's metatable, not the key's), so both examples assert that no entry keyed by the secret was stored; with the old nil-only checks both would hold one.
  • forever_spec: the Damage Prevented aura tracker listens for none of its three events on Forever or on a client with no combat-log reader, and for all three on 11.x retail.
  • split_spec: the tab window, 14 examples: off until opened, the tab's display in its own window with the main window unsplit, bars parented to and inside that window, the menu entries, the close button, Alt+Z, updating with the main window hidden, tab close and move, alongside the split view, the detail window, resizing, reopening at its saved place, a stale saved tab, and the retail damage-meter renderer with the main window hidden.
  • 893 passed, 0 failed across the whole suite.

[v1.19.2] (2026-09-29) - Tabs and a split view on the main window

New

  • Tabs on the main window. A row of folder tabs sits on top of the main window. Each tab is one display mode, labelled with a short name ("DD", "DPS", "Thr"; initials or the first four characters for a mode without one, cut on whole UTF-8 characters). Clicking a tab is SetMainWindowMode, so the window itself is unchanged. The title menu, the arrows, Alt+scroll and the keybindings all change the tab you are on, and its label follows. + adds a tab (starting on the first mode), with no limit. When the tabs do not fit, < and > appear at the ends and scroll the row, as does the mouse wheel over it; selecting a mode brings its tab back into view. Right-click a tab for Close (never the last one) and Move Left / Move Right. Tabs are saved per profile in MainWindow.Tabs by mode LABEL, not index, because indices move when a tracker module is switched on or off; LoadMainWindowData now follows the active tab's label for the same reason. An existing player starts with one tab showing the mode they already had. The window's clamp rect counts the tab row, so dragging the window to the top of the screen cannot push the tabs off it. Show Tabs in the window settings (MainWindow.ShowTabs, default on) hides the row. Designed with the operator on 2026-09-29 from a player's request to watch two displays at once; the split view below builds on them. The tabs are the game's own chat-tab art (Interface\ChatFrame\ChatFrameTab-BG* / -Selected*, used by FloatingChatFrame.xml in every flavour's tree), tinted with the window's title colour so they read as folder tabs of the red title bar and follow the colour settings; the selected tab is lit and solid, the others dimmer. They are drawn one frame level BELOW the window (re-applied whenever WindowOrder.lua raises it) and seated 9 units below the frame's top edge, so the red title bar covers their bottom edge and they read as folder tabs of it; the window's own hit rect is inset by the same 9 while tabs show, so a click on a tab's lower part reaches the tab rather than dragging the window. The 9 was tuned by eye from in-game screenshots (the first build floated above the bar, the second sat on top of it) and is not measured at a known UI scale. Drawing a child one level below its parent is Blizzard's own pattern (FloatingChatFrame.lua:1580), and a positive top clamp inset is how the chat frame takes in its own tab (FloatingChatFrame.lua:70), both per Peer Review. Seen on Classic Era by the operator on 2026-09-29.

  • Split view: a second display under the first. Right-click a tab and choose Show Below, and that tab's display fills a second set of bars in the lower part of the window, under a thin title-coloured divider that names it. Drag the divider to move the split (not on a locked window); the drag stops by itself once the left button is up (IsMouseButtonDown, as Blizzard's chat-tab drag does), so releasing with the cursor off the divider cannot leave the split following the mouse, per Peer Review, whether or not the client delivers OnMouseUp there (not verified in a client). Right-click it, or the tab, for Hide Below. Off until a tab is picked, so the window looks as it did. The window's bar drawing now works per pane: a pane is Recount.MainWindow itself (so every existing reader of Rows, DispTableSorted, GetData and the rest is unchanged) or Recount.MainWindow.Lower, a table with the same fields, its own scrollbar (Recount_MainWindow_LowerScrollBar), bars (Recount_MainWindow_LowerBar<n>) and total bar. RefreshMainWindow's body is now RenderPane, run once per pane; ResizeMainWindow lays both out, keeping at least one data bar (plus the total bar when shown) per pane, and drops the split while the window is too short for two panes without forgetting it. The file-local MaxValue is now per pane. The lower pane re-reads its tab's mode by label on every refresh, like the tabs, and clears its tables when that mode or the fight changes. Clicking a bar opens the detail window, graph, realtime graph or bar menu on THAT pane's display, and the detail window's mode arrows keep to it (MainWindow.DetailPane). Report, the title menu, the arrows, Alt+scroll and the keybindings still act on the selected tab (the top pane). Retail and Forever draw both panes from the damage meter (Tracker_Midnight.lua's renderer now takes a pane, through Recount:MainWindowLowerPane). Saved per profile in MainWindow.Tabs.below (a tab index, kept right when tabs close or move; closing that tab turns the split off) and .splitRatio (default 0.5). The Show Scrollbar setting covers both scrollbars. ResetData now clears every pane through FullRefreshMainWindow; it had cleared only the top pane's tables, so the lower pane kept showing the reset players (their records still carry fight data), found by the session's self-audit and pinned by a spec that fails with the old two lines. UpdateBarTextColors now walks every created bar of both panes rather than 0..RowsCreated, which skipped every bar once the total bar was switched on after the others existed (creating bar 0 set RowsCreated to 0). Seen working by the operator on 2026-09-29 on Classic Era (Damage Done over Damage Taken) and on WoW Forever; retail Midnight not yet seen.

  • Display mode: "Interrupted" -- who got interrupted, and by whom. The interrupt tracker recorded only the interrupter (Interrupts, InterruptData); the receiving side of SPELL_INTERRUPT ran only housekeeping, so "who keeps interrupting me?" could not be answered. AddInterruptData now also adds Interrupted and InterruptedBy (interrupter -> interrupted cast) to the victim, as Dispels keeps Dispelled / WhoDispelled, and InitFightData sets both up. Starts recording from this version. Like Interrupts and Dispels, it is not carried by LazySync (which sends damage, healing and active time only), so a grouped player sees only what their own combat log recorded. From the recovered self-raised audit finding of 2026-08-18 (inbox 1ec972ac). Its "CC Broken On" half is not built: the CC-break event's receiving side is the crowd-controlled mob, not the player who cast the CC, and the event never names that caster. So the mode would list mobs and still could not answer "who broke my Polymorph".

Changed

  • LibDBIcon-1.0 is now a required dependency instead of a bundled copy, on every flavour except WoW Forever. The six non-Forever TOCs add it to ## Dependencies, drop the libs\LibDataBroker-1.1 and libs\LibDBIcon-1.0 loads, and drop both from X-Embeds; .pkgmeta lists libdbicon-1-0 under required-dependencies, so the CurseForge app installs it. The standalone brings LibDataBroker-1.1 with it (its own embeds.xml). A bundled copy only updated when Recount re-released; the standalone updates for every player. Recount_Camelot.toc keeps loading the bundled copies, because the standalone's TOC does not list Forever's Interface 16001, so the files stay under libs/. No Lua changed. Known cost: a player who installs by hand must install LibDBIcon-1.0 too. Not yet run in a client. Requested by TOGTools on the user's 2026-09-25 decision.
  • Every Recount tooltip is placed by one function, Recount:AnchorTooltip (Widgets.lua), so none covers the thing it describes: below the hovered frame in the top half of the screen, above it in the bottom half, and aligned to its left or right edge in the outer thirds. It is the rule the minimap launcher already used (a copy of LibDBIcon-1.0's getAnchors), moved out of GUI_Minimap.lua and applied to the main window's bars, title, Compare button and tabs, the settings window's icon tooltips, and the compare graph's controls. The compare graph's value readout still follows the cursor, because it reports the point under the pointer. Asked for by the operator after a tab tooltip covered the tab row.
  • The addon-list title is the fleet's orange (|cffFF8000Recount|r) in all seven TOCs.

Fixed

  • UnitIsTrivial is called only if the client has it (Recount.lua, where a hostile mob is classed Trivial / Boss / Nontrivial). It is documented in the live and Forever trees only; the Classic trees' UnitDocumentation.lua does not list it. Whether the Classic clients have it anyway is not verified, and Recount has called it unguarded there for years, so this may change nothing in practice; without it a mob is Nontrivial. Every flavour.
  • Recount:GetModeIndex no longer reads L[modestring], which made AceLocale report a missing entry for any label that is not a locale key (every mode label on a non-English client). It reads the locale with rawget. Found while auditing the tabs. Nothing in the addon calls it (the keybindings use SetMainWindowModeByLabel, whose labels are all locale keys), so no player met the error; only specs and outside callers could.
  • The Titan Panel / broker display (GUI_TitanPanel.lua) is localized. Its stat and dataset labels, menu headers, broker label and tooltip lines were hard-coded English; they now read L[...], reusing the existing keys and adding nine (Recount Stats, Display Stat, Dataset, Overall, Showing, Left-Click, Right-Click, Toggle window, Configure display) to enUS, which every other locale falls back to. Found in Peer Review's single-pane read. Every flavour.
  • The broker's Dataset menu now switches the data set the way the window's own fight menus do. It only set CurDataSet, so the main window and the detail window showed the old set until the next refresh, and the death log stayed on the old fight. It calls the new Recount:ShowDataSet(set, fightName) (GUI_Main.lua), which sets the fight name, updates the detail window, clears every pane (FullRefreshMainWindow), refreshes and points the death tracker at the new set. The six fight-menu entries in GUI_Main.lua still repeat that sequence inline and clear only the top pane's tables (the lower pane catches up on its data-set check); they were left alone. A split_spec example checks both panes empty and refill through it.

Development

  • Language-server warnings cleared. .luarc.json declares ChatFrame1 and TooltipBackdropTemplateMixin, which the bundled LibDropDown reads. LibDropDown's options builder no longer assigns del()'s return value back to sortedOpts and lookup; it calls del and then sets each to nil. del already returned nil, so the behaviour is unchanged.
  • wow-version-replication.ps1 no longer deletes a file from the other installs while it still exists here. An editor's save-by-replace removes the file for a moment; if the watcher's poll caught that gap it deleted every replica, then re-copied on the next poll. Sync-File now treats a "Deleted" whose source is back as a change. Reported by TOGProfessionMaster's review. The watcher was restarted and holds its lock; a deliberate save-by-replace was not reproduced.

Testing

  • Eight weak assertions fixed: each checked that a key spelled as a literal was absent, which a typo would also pass. The key is now derived from the code, or the same key is also checked in a case where it has to be present. colors_spec compares the client's whole class-colour entry before and after GetColor; toc_spec refuses any field spelled like OptionalDeps, whatever its case; lazysync_spec also checks another player's version survives.
  • toc_spec covers Recount_Camelot.toc, which it never read. Forever joins the per-flavour checks (interface 16001, same title, saved variables, no OptionalDeps), keeps Ace3 and VersionCheck-1.0 as its only dependencies, must load the retail file list plus the two bundled libraries, and is the only TOC allowed to load or declare them.
  • The harness pin moved ff6e593 -> 870c83d, and Tests/env_recount.lua hands back everything it now provides (inbox thread 0fb44ef9, all six batches). Deleted: the unit readers (health, power, dead/ghost/feign, friend/attack, party/raid, threat), GetNetStats, the declension pair, GetInstanceInfo, GetZonePVPInfo, C_Scenario, C_PetBattles, IsEncounterInProgress, the combat-log readers, ReloadUI, CopyTable, GetMouseFocus, MouseIsOver, GetMouseFoci, GetSpellLink, the string and colour constants, the whole COMBATLOG_OBJECT_* / SCHOOL_MASK_* table, FauxScrollFrame_*, MinimapCluster, GetNumDisplayChannels, and our C_DamageMeter. Also gone, because nothing in Recount calls them: RegisterAddonMessagePrefix, the bare GetSpellName, GetMinimapShape (LibDBIcon falls back to "ROUND") and IsInScenarioGroup (deletion.lua checks for it). What stays is what the harness leaves out on purpose: UnitIsTrivial, YELLOW_FONT_COLOR, and the pre-Settings InterfaceOptionsFrame pair for the legacy-settings spec. Measured by resetting the harness modules without env_recount and checking each name on Classic Era and Mainline. The Classic-only globals (GetSpellLink, GetMouseFocus, MouseIsOver, SCHOOL_MASK_*) are now absent on Mainline, as on the client, and the suite stayed green.
    • env.setDamageMeterSession now writes the harness's wow.damageMeter model, and indexes each source by GUID and creature ID, since the harness keeps sources in their own table. A secret GUID is left out of that index. Our C_DamageMeter had been silently overwritten on every Mainline switch, which is what failed 36 retail and Forever examples at the new pin.
    • Two specs changed with the harness's answers: deletion_spec sets wow.zone, because the harness's GetInstanceInfo names the zone from it and an empty zone is Recount's "not loaded yet, retry" path; lazysync_spec stubs UnitIsConnected for its stubbed raid, which Recount asks by player name.
    • 816 passed, 0 failed. Recount.lua line coverage is 84.04%.
  • modes_spec covers "Interrupted": the fixture adds a mob interrupting the healer, and two examples check the count, the interrupter and the interrupted cast, and that the mode's detail view reads InterruptedBy. The per-mode loop picks the new mode up on its own. 823 passed, 0 failed.
  • Tests/tabs_spec.lua, 20 examples: the first tab mirrors the existing mode; + adds and selects; clicking a tab switches mode; other ways of changing mode relabel only the active tab; a tab keeps its mode when switching a module off shifts the indices; close, never-the-last, move with the selection; the right-click menu; the overflow row (< and > appear, hidden tabs, the selected tab in view, scrolling by arrow and wheel, arrows gone once wide enough); Show Tabs off; the short-name rules. Added after the self-audit and Peer Review: the tabs stay one level below the window when it is raised and never go negative; a row sized 4 units longer than the row but shorter than the window keeps every tab and the + inside it (red with either the inset fix or the trailing-gap fix reverted, measured); the Show Tabs checkbox lies inside its settings page; every mode label has an entry in the short-name table, on Classic Era and Mainline, so a new mode fails the suite instead of silently getting initials. 848 passed, 0 failed.
  • Tests/split_spec.lua, first 17 examples: off by default; a data reset empties both panes; the lower pane shows the picked tab's display while the top follows the selected tab; the panes sit either side of the divider and inside the window; each pane's total bar; Show Below / Hide Below from the tab menu; closing, moving and closing-another-tab keep the lower tab right; a window too short for two panes drops the split and a taller one brings it back; dragging the divider with the harness's cursor grows the top pane and stops on release; a locked window does not drag; a click opens the detail window on the clicked pane's display (red with that routing reverted, measured); each pane's tooltip; a saved lower tab that no longer exists is repaired; retail Midnight draws both panes from the damage meter, and none below when the split is off. A menu entry that fades its menu shut raised offline, because UIFrameFade (every client's Blizzard_SharedXMLBase/FrameUtil.lua) was missing from the harness at 870c83d; a yielding stand-in covered it until the harness shipped the whole family.
  • The harness pin moved 870c83d -> 27b84b3 -> 1867896 -> 6820bd5 and the UIFrameFade stand-in is deleted (inbox a4066a79). At 27b84b3 a frame with no height and a single anchor has no rect, as in the client, and GameTooltip had no height, so four tooltip-placement examples read nil; 1867896 sizes the tooltip from its lines (inbox f3ef297d) and they pass with no Recount-side workaround. 6820bd5 reports a spec file that hits the runner's time limit as a named TIMEOUT instead of crashing with (error object is not a string) (inbox 4fb957ea).
  • The suite no longer keeps every earlier copy of Recount alive. env.initialize now drops Recount* globals whose frames came from an earlier reset (dropStaleNamedFrames), as a /reload empties _G. A named frame only some specs create, such as the compare window's RecountCompare_FightDropDown, kept its whole copy of Recount reachable through its initializer's upvalue (found by walking references from _G), and those copies went on receiving events. Across a full run the live frames grew from ~4,600 to ~47,000 and the heap to 140 MB, so modes_spec, run late, spent ~240 ms per example collecting garbage and passed the runner's 60-second limit. Now the frames stay at ~4,600 and the heap at ~17 MB throughout, modes_spec takes 13 s, and the whole run about a minute instead of about four. Clearing the names BEFORE the new load was tried first and broke 59 retail examples: a still-referenced old copy got an event mid-load and looked its scroll bar up by a name that was gone. The older "tooltip above the tab row" example, which contradicted the tooltip rule above, is removed with the operator's approval. 868 passed, 0 failed at the default limit.
    • env.initialize now fails with a named error when more than 12,000 frames are still live after the harness's reset (LIVE_FRAME_LIMIT), so a future leak fails the suite at its cause instead of timing out whichever spec runs late. Asked for by Peer Review.
  • split_spec gains four examples (21 in all). One checks that Recount:ShowDataSet empties and refills both panes (see Fixed). Three come from Peer Review's reply: the divider drag stops when the button comes up off the divider, pressed and released through the harness's wow.mouseButtonsDown (the harness pin moved 6820bd5 -> b1f9386, which ships IsMouseButtonDown at Recount's request, inbox 7494ccfc; the stand-in env_recount carried until then is deleted); a fight fought after a data reset shows on the Current Fight view; and the lower pane follows both combat edges, showing the finished fight after combat ends and emptying when the next one starts. The combat-edge example first failed on its own setup: it fed the fight through TimeTick, and with nobody in combat by the test roster each tick ends the fight after one second of GetTime, which Recount discards as under three seconds (LeaveCombat -> CopyCurrentFights). The fight is now fed without ticks, and every assertion is unchanged.
  • locale_spec checks every L["..."] literal in the files the Classic Era and Mainline TOCs load has an English entry. enUS is AceLocale's default locale, so it fills every gap in the other languages (the split view's three new strings show in English on a non-English client, per Peer Review's reading of AceLocale-3.0.lua:62-66 and :110-118); a key missing from enUS itself is the only one that would reach AceLocale's missing-entry path on every client. The scan finds over 500 reads and none is missing. It sees only literal keys: a computed read such as L[someVar], the shape of the GetModeIndex fix above, is invisible to it, and the spec says so.
  • 873 passed, 0 failed across the whole suite at pin b1f9386.

[v1.19.1] (2026-09-27) - Forever login error

Fixed

  • Forever: Recount errored on login, "zonefilters.lua:102: attempt to call a nil value". Reported from a Forever client. UpdateZoneGroupFilter called the bare GetZonePVPInfo, which Forever does not have; it ships only C_PvP.GetZonePVPInfo (wow-ui-source-forever, PvpInfoDocumentation.lua). The file-local now takes C_PvP.GetZonePVPInfo when the client has it and the bare global otherwise, so the older clients are unchanged. A spec covers the Forever shape, with the bare global made to raise.
  • Forever: the other bare globals it lacks, found by comparing every global Recount's Forever file list reads against wow-ui-source-forever. Not yet run in a Forever client.
    • COMBATLOG_OBJECT_* exist on Forever only in Blizzard_DeprecatedCombatLog, which returns early unless the loadDeprecationFallbacks CVar is on. Recount.lua's FlagsToUnitID uses them as table keys while the file loads, so with the CVar off it would raise "table index is nil"; CreateOwnerFlags and AddCombatant add and mask them. Each now has an or literal like the flags above it.
    • Chat reports: SendChatMessage and BNSendWhisper are deprecation fallbacks on Forever too. GUI_Main.lua and GUI_Detail.lua now take C_ChatInfo.SendChatMessage / C_BattleNet.SendWhisper where the client has them (same arguments) and the bare globals otherwise.
    • The report window's RealID target called BNGetSelectedFriend / BNGetFriendInfo, which Forever does not have. The target is left off the list on a client without them.
    • The bundled LibDropDown called GetMouseFocus when the pointer left a submenu entry. It is gone from Forever and from retail 11.0+; the library now falls back to GetMouseFoci()[1].

Development

  • wow-version-replication.ps1 replicates into _classic_beta_, the folder WoW Forever installs to (WowB.exe), so a Forever client picks up the working copy the same way the other flavours do. Verified with -DryRun, which lists it as a fourth target, and by the restarted watcher populating _classic_beta_\Interface\AddOns\Recount.

[v1.19.0] (2026-09-23) - World of Warcraft: Forever

New

  • World of Warcraft: Forever (the _Camelot client, interface 16001). Recount_Camelot.toc loads the same file list as the Mainline TOC. Forever is the retail client -- WOW_PROJECT_MAINLINE, COMBAT_LOG_EVENT_UNFILTERED flagged HasRestrictions, and the same C_DamageMeter as Midnight (wow-ui-source-forever 1.60.1.69977, CombatLogDocumentation.lua, DamageMeterDocumentation.lua) -- so it takes the Midnight data path. The Midnight gate in Recount.lua, GUI_Main.lua, Tracker_Midnight.lua and GUI_Detail_Midnight.lua was interface >= 120000, which Forever's 16001 fails; it is now >= 120000 or < 100000, which leaves 11.x retail on the combat log. The test is made once, in Recount.lua, which publishes Recount.IsMidnightClient and Recount.IsRetailGame; the other four files read those instead of keeping their own copies, so they cannot drift apart. Without it Forever would have registered the restricted combat log at login, which is what threw ADDON_ACTION_FORBIDDEN on 12.0. Not yet run in a Forever client.

Fixed

  • Forever: a click on a bar in combat would have errored. DamageMeterCombatSource.sourceGUID is ConditionalSecret on Forever (live leaves it unmarked), and the spell-detail window passed it straight back into C_DamageMeter.GetCombatSessionSourceFromType, whose SecretArguments = "AllowedWhenUntainted" refuses a secret from addon code. A secret GUID is now dropped before the call: an enemy is still found by creature ID, and a player with a hidden GUID shows the "available after combat" placeholder. The comments that called the GUID NeverSecret are corrected.
  • Forever: the class-colour settings listed Death Knight, Monk, Demon Hunter and Evoker, because they keyed on the retail client rather than the retail game. Forever has the original nine classes.
  • IsInScenarioGroup is called only if the client has it. Nothing on this box says whether Forever keeps it, so the retail branch of deletion.lua checks rather than assumes.
  • Retail Midnight and Forever: the spell breakdown could error in combat on a hidden spell ID (audit finding 34). DamageMeterCombatSpell.spellID is not declared NeverSecret in the client's schema, and the getter is SecretWhenInCombat, but the breakdown used it as a table key unchecked -- the same "cannot be indexed with secret keys" error Recount users reported from the aura code. A secret spell ID is now skipped exactly like a secret amount (the window shows its "available after combat" state), and on the enemy side it is left out of the per-spell index while the enemy is still counted. The comment that called the field "never a secret" is corrected. A second comment, in the mitigation module, gave a reason for its isFullUpdate check that the schema rules out (finding 35); the check is unchanged and the reason now matches the schema.
  • The bar-texture dropdown could be handed a texture name where it expects a list position. One variable held the saved texture's name and was overwritten with its list index only when found, so a saved texture no longer in the list (its media addon removed) reached UIDropDownMenu_SetSelectedID as a string. The two are separate now; a missing texture selects nothing. Every flavour.
  • Reset Colors wrote into Blizzard's own class-colour table. ResetDefaultClassColors set a = 1 directly on the RAID_CLASS_COLORS entry (or a class-colour addon's CUSTOM_CLASS_COLORS entry) before storing it. It now stores a copy. Every flavour.

Testing

  • Tests/forever_spec.lua: the gate on Forever and on 11.x retail, and the secret-GUID guard, with a spec double that refuses a secret GUID the way the client does. With the guard removed, the three guard examples fail.
  • The harness pin moved 1f8fe09 -> e8a88cd (316 commits). One Recount-side break: installGlobals replaced _G.Enum wholesale, which deleted the harness's Enum.RegisterAddonMessagePrefixResult and failed 34 lazy-sync examples inside the harness's C_ChatInfo. It now extends Enum.
  • The private test environment now overrides nothing the harness defines. Three harness defects found along the way -- IsInGroup ignoring its group category, and AbbreviateNumbers and FontString:SetText refusing a secret the client accepts -- were reported, wrapped locally for a few hours, fixed by the harness in 2c5f7fc, and the wrappers deleted. The pin is now ff6e593, which also delivers harness item 12 (text width scales with font size); no example moved. Handed back: the stubs it overwrote, the private bit library, C_Timer (whose stand-in only recorded calls and never ran one), ToggleDropDownMenu, the group, zone, guild and realm readers, RAID_CLASS_COLORS, SendChatMessage, the spell readers (specs now register the spells they cast, since the harness answers nil for an unknown spell as the client does), the per-flavour builds (Recount keeps one override: Mainline runs as 12.0), SlashCmdList, and the settings backend (the string-addressed stand-in survives only for the one example that models a client without C_SettingsUtil). env.time, env.locale, env.printed, env.chatMessages and env.openedCategories are views onto the harness's own records. What remains private is 59 globals the harness does not define yet (requested, and sequenced by WoWAPITesting). Deleting the colour stub exposed a spec asserting a fake MAGE colour (0.41; Classic Era's is 0.25).
  • Secret values in the specs are real now. All 13 examples that used Recount's by-value registry (env.markSecret) use the harness's wow.secretValue, which raises on any arithmetic or comparison, and the registry is deleted. Under the old model a "secret" was an ordinary number that arithmetic still worked on, so a missing guard could not fail an example -- the gap round 8 of the audit named. The Midnight bar renderer, the spell-detail window and the mitigation module all pass with values that raise on contact. Two harness display sinks raised on a secret the client documents as accepting (AbbreviateNumbers and FontString:SetText, both AllowedWhenTainted); the harness fixed both in 2c5f7fc.
  • Peer review and harness requests moved to writ's inbox. docs/AUDIT.md and Tests/HARNESS_CONTRACT.md were imported into the inbox and deleted (135a238); their full text stays in git history, committed first in 31e638d.

[v1.18.8] (2026-08-25) - the reset button actually resets on retail Midnight

Fixed

  • Retail (Midnight): the Reset confirmation's Yes button did nothing. Reported from a live client and reproduced here: the reset icon opened "Reset Recount? Do you wish to reset the data?", and answering Yes left every bar exactly where it was. The reset routine clears Recount's own combatant tables -- and on 12.0 those tables are empty, because the client took the combat log away and the numbers on screen are read live out of Blizzard's own meter session instead. So the reset was wiping a store that held nothing and then re-drawing the window from the session it had never touched, which on screen is indistinguishable from the button being dead. The reset now also clears the game's combat sessions, through the same call Blizzard's own damage-meter window uses for its "reset all sessions" menu item, and it runs before the redraw so the window comes back empty rather than being repainted from the data we just failed to clear. Confirmed working in a retail client.

    Every Classic flavour and pre-12.0 retail are untouched: the new step is gated on Midnight, does not exist at all on the Classic builds, and reports failure quietly rather than erroring on a retail client whose meter API is absent.

Documentation

  • The README and the CurseForge description were audited against the code, and both were wrong about reporting. Each claimed the report window offers "Say, Party, Raid, Guild, or a custom channel". The real list (GUI_Report.lua:34-50) is Say, Party, Instance, Raid, Guild, Officer, RealID, Whisper and Whisper Target, plus every chat channel you are joined to, with destinations you cannot currently use omitted from the list. Both now say so.

  • Two shipped features were documented nowhere. Ctrl+click a bar pops out a live per-player graph -- it has been in the bar's right-click menu as "Show Realtime Graph (Ctrl Click)" the whole time and in neither document. And the live meter windows were one line under Reporting on the CurseForge page, which is also the wrong section; they now have their own, naming what they can actually track (per player, the whole raid's DPS/DTPS/HPS/HTPS, and FPS / latency / traffic / remaining addon bandwidth). Also added: how to drive the window at all -- the CurseForge page had no click, shift-click, right-click, alt-wheel or resize instructions anywhere -- along with Keep Only Boss Segments, the who/where recording filters, and the auto-clear-on-instance/group options.

  • The Midnight explanation now has a permanent home instead of living in a patch note. The one full account of what does and does not work on 12.0 sat inside the v1.18.6 entry under Recent Updates, a section this repo holds to the last five patches -- so it was two releases from being deleted, taking the explanation with it. It is a standing section now, updated for what has changed since: the breakdown naming its targets, and reset clearing the game's own sessions.

Testing

  • Five examples cover the reset path on the Midnight flavour, and the first one is the bug. It renders a session, asserts a bar is on screen, resets, and asserts the session is gone -- not the combatant table, which was always empty and is precisely why the old code looked correct. The rest drive the confirmation dialog's Yes button end to end (the actual reported entry point, which no example had exercised), assert the window is left with no bars, assert a client with no meter API reports failure instead of throwing, and assert the whole thing is absent on Classic. The offline environment gained the ResetAllCombatSessions half of C_DamageMeter, which it had never modelled.

[v1.18.7] (2026-08-25) - the retail 12.0 fixes: secret aura data, a removed mouse-over helper, an updated LibGraph, and a spell breakdown that finally names what you hit

Fixed

  • Retail 12.0: Recount no longer throws every time an aura changes on you or anyone in your group. Reported as 27 errors in one session: TrackerModule_Mitigation.lua:198: attempt to perform boolean test on field 'isFullUpdate' (a secret boolean value, while execution tainted by 'Recount'). The 12.0 client can hand an addon aura data whose fields are secret -- readable enough to hand straight to the game's own display widgets, but illegal for an addon to test, compare or use as a lookup key. Damage Prevented does all three: it asks whether an update is a full refresh or a delta, and it files each aura under its spell id and its instance id. Each of those is now checked first and skipped when the client will not let us read it, so the tracking simply goes quiet on that client instead of erroring. isFullUpdate was only the first line reached -- the two ids sit two lines further down and would have thrown next, so all four reads are guarded rather than just the one in the report.

    What this costs, said plainly: on a client that makes the fields secret, Damage Prevented records nothing for the auras it cannot read. That is an undercount and never a wrong number, which is the same direction every other unknown in that mode is resolved. It affects retail only; no Classic flavour makes anything secret, and nothing about their behaviour changes.

  • Retail 12.0: the graph windows no longer error continuously while the cursor is near them. MouseIsOver -- the function that answers "is the mouse over this?" -- was removed from the retail client in 12.0, and four places in Recount grabbed it once when the addon loaded and then called it forever after. Because two of them are per-frame handlers, that is not one error but one per frame for as long as the window is open; the same defect in our embedded copy of LibGraph produced 148 in a single sitting. All four now call the widget's own IsMouseOver method, which is what the removed function did internally anyway and which every supported flavour has had all along -- so this is one spelling on all six versions, with no version check and no fallback. Affects the damage-over-time graph, the comparison graph, and LibGraph's pie chart.

  • Retail (Midnight): the spell breakdown no longer switches which spell it is showing you, without moving the highlight. Clicking a spell in the breakdown opens its per-target split below. The window remembered your choice as a ROW NUMBER, and the rows are ranked by damage -- so the moment another spell overtook the one you had selected, the refresh re-selected the same row, which was now a different spell. The highlighted row did not move, so nothing on screen said anything had changed: you would be reading one spell's name against another spell's targets. It now remembers the spell itself and follows it up or down the list.

  • Retail (Midnight): the spell breakdown now names the mobs you hit, and how much you did to each. Clicking a spell showed a single target row with no name against 0 damage. The damage-done data genuinely has no target side -- the game hands addons one target slot per spell there and leaves it blank -- but the information does exist, in a separate part of the meter that lists each ENEMY and, beneath it, the spells that hit it and who cast them. Read backwards, that is exactly "which mobs did I hit, and for how much", and the lower list is built from it now. Three things had to be right for it to work, and each was wrong in turn: the entries on that side are keyed by a spell id that comes back as 0, so a per-spell lookup matched nothing and the list stayed empty; the per-target figure lives on the spell entry's own total rather than in the target record, which is blank there too, so the row first arrived correctly named and reading zero; and the percentage was still being taken against the selected spell's total, which is not a share of anything once the list spans every spell. The lower list now reconciles with the Damage Taken view of the same fight.

    Two limits worth stating. Matching a spell to you is done on your name, because that record carries no unique id -- so during a raid or dungeon encounter, where the game hides names from addons, the match cannot be made and the list falls back to what it showed before. And where the game reports no usable spell id, the list is everything you damaged rather than just what the selected spell hit; that is broader than the row above it, never a wrong name.

  • Retail (Midnight): per-second figures below 1,000 no longer print all of their decimals. The breakdown's DPS column showed 161.82124328613 on one row and 5.2K on the row above it. The game's own number-shortening only shortens at 1,000 and above and hands anything smaller straight back, so a fractional rate arrived at the column with every digit intact. Rates are rounded to whole numbers before the column formats them.

  • Retail (Midnight): the spell breakdown's column headings sat outside the window. "Name of Ability", "DPS", "Damage" and the two below them were drawn above the window's top border, across the title bar and the close button. The window is a port of the Classic detail window, and the port dropped the container frame that everything in the original hangs off -- a frame 32 pixels shorter than the window, bottom-aligned, so that its top edge starts below the title strip. Every position inside is measured from that container, so with it gone the whole contents -- headings, rows and both pie charts -- were laid out 32 pixels too high. The container is back, so the layout matches the Classic window it was copied from.

  • Retail (Midnight): spells tied on exactly the same total no longer swap places on their own. With two spells on equal damage the ordering was left to chance, and this window re-sorts several times a second while it is open -- so tied rows could visibly trade places on a tick where nothing had happened at all. Ties now break consistently, so the list only changes when the numbers do. Ties are commonest at the start of a fight, when several spells have landed once each, which is exactly when the breakdown is most likely to be open.

Libraries

  • The bundled LibGraph-2.0 is replaced with the shared copy, and it now supersedes every other copy on the client. Recount embeds LibGraph-2.0 for the damage-over-time graph, the comparison graph and the pie charts. The copy shipping here is the merged shared build, carrying the retail 12.0 IsMouseOver fix, and it registers in a +2000 version band.

    That band is the point. Only one LibGraph is ever live in a session: every copy registers under the same library name, the highest version number wins, and each loser stops at its own first few lines without defining anything -- silently. Measured across this install on 2026-08-25, the field was Details at 90062 and Recount at 90068 (both stock, both carrying the crash) and FastGuildInvite at 91068 (a private fork, also carrying it). FastGuildInvite's was therefore the copy actually driving Recount's graphs, which is why the earlier fix to Recount's own copy changed nothing in game. At 92068 this one wins outright, so installing this release fixes the graphs even for players who also run an addon shipping an older LibGraph. The bands are now +0 stock, +1000 a private per-addon fork, +2000 the shared copy.

    Only the .lua changed: all eleven textures were compared byte-for-byte against the shared copy and are identical. The library also gains two hooks Recount does not use yet -- a label hook for localising the axis numbers, and optional X-axis labels -- which are additive and change nothing about current behaviour. This is a stopgap while the library goes out as a proper external dependency; the version bands stop mattering once there is only one copy to install.

Testing

  • The mitigation suite gained seven examples covering the two paths above. Six drive each secret field individually -- an unreadable full-update flag, a secret spell id, a secret instance id, a secret id during a full rescan, and a secret caster -- and each asserts the resulting state rather than merely that nothing threw, because the failure worth catching is a guard that skips the wrong thing. The seventh drives the module's own event frame, which is where the reported crash actually entered and which no example had exercised before: calling the tracking function directly cannot catch a dispatcher that stops forwarding the payload.

  • Harness pin moved b307a9b -> 1f8fe09, and contract 22 was withdrawn because it was already delivered. The window suite reddened the moment the IsMouseOver fix landed -- the shared harness stubs the removed MouseIsOver global and, at our pin, did not stub the widget method it forwarded to. Contract 22 was raised for it; the harness had shipped exactly that the same day, on the Region type so textures and font strings get it too, in an adoption entry that cites this addon's crash. Our pin was six entries behind, which is why the request was written at all. Whole suite green after the move. The staged stand-in in Tests/env_recount.lua stays for now and is written to yield on its own: every Recount spec loads that private environment rather than the harness's frame layer, so the harness's method does not reach it -- removing it is part of the environment migration already on the repo's todo list, not a loose end from this release.

    Also picked up in that range: the harness now runs after_each when an example fails. Previously one red example could leave a substituted global installed for every later spec file, turning a single genuine failure into a wall of unrelated ones. Recount's counts did not change; nothing here depended on it.

  • The Midnight suite gained eight examples, every one of them written from a defect seen in a live client rather than imagined. Both column headers are asserted to sit below the title strip (driven red by giving the container the window's full height, which reproduces the 32-pixel shift exactly); the enemy-side target lookup is covered by its real shape, including the spellID = 0 that made the first implementation match nothing, the amount that has to come from the spell entry rather than the target record, and a percentage that must be a share of the target list rather than of one spell; and a sub-1,000 rate is asserted to render as 162 rather than 161.82124328613.

    Two of them are honest about what they do not catch, which matters more than the count. The row-position example stays green under the mutation that reddens the header one -- the rows travel with the header, so their relative positions stay valid while the whole block is misplaced -- and that measurement is written at the call site so it is not mistaken for a second catch. Writing the aggregate example also found a real bug before the code shipped: the substitution was being applied inside the per-spell loop, so a second entry for the same spell appended into the shared aggregate table and corrupted the list for every other spell.

  • A fixture that could only produce the happy shape is why none of this went red earlier. The spell builder in the Midnight suite took a target name as a parameter and every call passed a real one, so the blank record the live client actually sends was never exercised. The general form is worth keeping: when a fixture builder takes a value as a parameter, ask what the live source puts there when it has nothing to put.

  • Reported the same LibGraph defect to FastGuildInvite, which embeds the same fork and produced the error the user saw, as finding S36 in its docs/AUDIT.md. Its copy is that addon's to fix.

  • Answered peer-review round 7 (findings 31, 32 and 33), each driven red before being fixed. Both breakdown-window fixes above had their production change reverted and the suite re-run to prove the new examples catch them: the selection example failed with the wrong spell under an unmoved highlight, and the ordering example failed with raw encounter order. The pre-existing selection example stayed green under both mutations, which is the reviewer's process point demonstrated rather than accepted -- it is retitled for what it actually pins. Finding 33's remedy is a shared env.widgetsCreatedBy helper in Tests/env_recount.lua, so the two spec files that ask "did this click build a menu" can no longer answer it two different ways; one of them had been counting the whole widget registry, which is both unsound against a weak-valued table and a weaker claim than counting what the click produced. Suite 758 -> 771 passed.


Older releases (v1.18.6 and earlier) are in CHANGELOG_ARCHIVE.md.

This mod has no additional files