Recount-v1.20.0
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 fromRecount:SpellSummonon everySPELL_SUMMON. A summon counts when its caster is a player whose class, fromGetPlayerInfoByGUID, isSHAMAN, 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 withC_Spell.GetSpellNameorGetSpellInfo; a summon counts when its spell name is one of those. The client loads some spell data on demand (Classic Era documentsC_Spell.IsSpellDataCachedandC_Spell.RequestLoadSpellDatainBlizzard_APIDocumentationGenerated/SpellDocumentation.lua), so an id that exists but has no name yet is asked for withRequestLoadSpellDataand retried on the next summon, instead of the table being built once and that totem going uncounted for the session (Peer Review, thread9df105ed, F2). Both functions are feature-detected; withoutDoesSpellExistan 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, thread9df105ed). 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, threadcf1a2066, 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 wrapsRecount:PutInCombatto 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 wrapsRecount: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 (threadcf1a2066). Loaded by every TOC, appended after Threat so existing mode positions do not move; short tab nameTot. 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:MergedPetAmountinRecount_Modes.luasums a field over the owner and every pet when Merge Pets w/ Owners is on, andRecount.MergedPetBreakdownexposes 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 aSPELL_DISPELfrom the totem is not verified for any flavour, so the CurseForge notes leave cleansing totems out until it is seen in game (Peer Review, thread9df105ed, 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.luanow pollspet,partypetNandraidpetNbeside their owners, keyed as the combat log stores a pet (PetName <OwnerName>, withName-Realmfor 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 nameLive, registered through the newRecount: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 newRecount.LiveModes, whichme:RenderPaneandRecount:ReportDatainGUI_Main.luaread (oneHasDataForhelper) 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, threadef07ad9f, 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 byRecount:ShowsCombatantinGUI_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; whetherUnitDetailedThreatSituationgives it a reading on Retail Midnight or Forever is not verified. ThatUnitName("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 inGUI_Config.lua, switches on global data collection (throughRecount: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, threadef07ad9f, 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 inRecount.luaand 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 sharedaddPetTooltipinRecount_Modes.luareplaces the two copies. - Power gains out of combat are dropped when Record Combat Only is on.
Recount:AddGainrecorded 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 inAddHealData, 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 inRecount:TimeTick, earlier fights included. Found by Peer Review (threadeac2bef3, 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.LiveModestellsme:RenderPaneandRecount:ReportDatainGUI_Main.luathat 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 useRecount:FormatLongNums. Peaks stay saved in thousands but are now kept to the single point (KThreatdivides to 1/1000, not 1/10), so the Threat bar shows the same 6,294 Live Threat does instead of 6,300 (Peer Review, thread2af65df6, 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;RescaleSavedThreatrounded 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, threadeaa2b2be, S1; confirmed by Peer Review).RescaleSavedThreatreturns at once on aTHREAT_SCALE1 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, thread2288371d, 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 newRecount.LiveModeSubjects, keyed by mode likeRecount.LiveModes, so a second live mode would name its own subject instead of the threat target (threadeaa2b2be, 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:LayoutPanestill 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 newRecount.GraphClickModesmakesme:MainWindowSelectPlayeropen 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'sUnitDetailedThreatSituationpercentage, 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:OpenModeDropDowninGUI_Main.luanow opens Damage, Healing, Threat, Power, Utility and Misc as LibDropdown submenus (nestedtype = "group"entries), in that order, from oneModeGroupstable. 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 explicitorderrather 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 keysPowerandUtility;Miscwas already translated. In game the operator found a group that highlighted on hover and showed no submenu:FadeMenufades a picked-from menu to alpha 0 withUIFrameFadeand then releases its frames to LibDropdown's pool, andAcquireFramehanded 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'sfinishedFuncfrom under its new menu.AcquireFrameinlibs/LibDropDown-1.0/LibDropdown-1.0.luanow cancels any fade on the frame (UIFrameFadeRemoveFrame, which Classic Era and retail both define inFrameUtil.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 ininteractions_spec.luahover 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 needGameTooltip_SetDefaultAnchor, which the harness supplies since pin0714030(asked for on inbox threadfcf6a420); until thenTests/env_recount.luacarried 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:BarDropDownOpenandRecount.CreateBarDropdown. The older UIDropDownMenu bar menu, replaced long ago by the LibDropdown menuRecount:OpenBarDropDownopens, had no caller in Recount, and none of the addons installed under Classic Era call either method (Peer Review's search, thread4dd1717d); other flavours' AddOns trees were not searched. A plugin that still calls one of them now raises. The two unusedUIDropDownMenu_AddButtonandUIDropDownMenu_SetAnchorlocals 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 removedBarDropDownOpen; they are onePlaceMenuBeside, pinned by a placement spec per menu ininteractions_spec.luaandguimain_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:CreateFightDropdownand the commented-outFightDropDownOpen,ModeDropDownOpenandCreateModeDropdown(no caller);Recount.ShortNumber's duplicate T, G, M and k branches;me:SetBar's row-range guard (its two callers inme:RenderPaneonly 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 atruelocale value (AceLocale-3.0 storestrueas the key,AceLocale-3.0.lua:50and:65; a newer AceLocale could change that); andSetTabLevel's no-tab-strip guard (only called on the main window once the strip exists).me:AddCombatantToGraphDatais folded intoRecount:AddAllToGraph(below). NewTests/guimain_spec.lua(75 examples) takesGUI_Main.luato 100% line coverage (Peer Review, threadf4093173, S4).- With Merge Absorbs on, the Healing tooltip's pet share now counts the owner's absorbs. The bar adds them (
MergedPetHealingDPS) butaddPetTooltip's denominator did not, so the pet's percentage read high. Found by Peer Review (threadeac2bef3, 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 itsdownscaleThreatoption, which defaults on and is described as "downscale threat so 1 damage = 1 threat" (dfherr/ThreatClassic2core/core.luaUpdateThreatDataandcore/config.lua, read 2026-10-17).TrackerModule_Threat.luadivided 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 byTHREAT_SCALE: 1 whereWOW_PROJECT_ID == WOW_PROJECT_MAINLINEorRecount.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 ofRecount.LocalizeCombatants), peaks and TPS series both, on Classic clients only. Each combatant is markedThreatScaledonce 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 thantrue, so a later correction to the scale can tell what saved values were divided by (Peer Review, threadef07ad9f, F4).MoveFightspointsLastFightDataandFight1at 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
targettargetThreatClassic2 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:RenderPaneinGUI_Main.luadropped 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;ReportDatanow setsMaxValuefrom the top row it sorted, through onesortRowshelper thatme:RenderPaneshares so the two cannot drift apart (Peer Review, thread28e0c74f, B2). Rows from a previous mode or fight cannot reach a report:Recount:SetMainWindowModeand every fight-menu choice empty both tables first. Found answering Peer Review's question on the self-audit (thread36c733ea). - 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 onTracking["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 (thread2af65df6, P1). - A shaman from another realm got no credit for Damage Prevented.
TrackerModule_Mitigation.luaread the Stoneskin caster withUnitName, which returns the realm separately, while combatants are keyed by the combat log'sName-Realm. The lookup now joins them through the newRecount:UnitCombatantNameinroster.lua, which the threat module andRecount:FindOwnerPetFromGUIDuse too, in place of three spellings of the same join (Peer Review, threadef07ad9f, F5). It joins only a non-empty realm and never on Forever, whereUnitName's second return is a surname. Forever is tested asIsMidnightClient and not IsRetailGame: my first version skipped onIsMidnightClientalone, 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).FindOwnerPetFromGUIDhad joined any second return, so an empty realm gave the ownerName-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'sNameUtil.FormatUnitNameForDisplaytakeslocal name, surname = UnitName(unit)and joins the two withCHARACTERNAME_SURNAME_SEPARATOR(wow-ui-source-foreveratbd2470a,Blizzard_FrameXMLUtil/Camelot/NameUtil.lua; found by Peer Review, threada4876f8a). The same file says that withRegionalUniqueNamesEnabled()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, andFindOwnerPetFromGUIDis reached only fromRecount:DetectPeton the combat-log path, so the Forever half of this entry is a correctness fix with no visible effect today (thatDetectPetnever 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:DeleteGuardianOwnerByGUIDindexedGuardianReverseGUIDswith the record it had just read from it, which matched nothing, so it never removed a GUID. It now walks each record's GUIDs, withpairsbecause 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:MergedPetAmountandRecount.MergedPetBreakdownindexed the fight row unguarded. Both helpers now read a missing row as empty. Found by Peer Review (thread71c1a081). - 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 inRecount_Modes.luaand the seven modules now gets the row from oneRecount:CurrentFight(data), which returns nil when there is no combatant, noFightstable 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, threaddca25a1a, F3). The main window's bar and report loops inGUI_Main.luause it too; two of their three copies skipped theFightscheck. 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:ActiveTimereadTimeHealandTimeDamageoff the same unguarded row. Found by the newmodes_specsweep below, which raised for each. - The graph window's "add all" showed only one line when the graph was closed.
Recount:AddAllToGraphcalledme:AddCombatantToGraphDataonce per listed combatant, and each call restarted the comparison when the graph window was hidden. The window is only shown by theSetGraphDataafter the loop, so every combatant restarted it and only the last one was plotted.AddAllToGraphnow 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, threaddd45be6d, X1; pinned by a spec). The existinginteractions_specexample checked only the graph's title, so it never saw this;guimain_spec's "adds each listed combatant" failed on the old code (desk run20261018-083332.940) and passes now. Found writing that spec (thread4dd1717d, A1). - "Add all" to the graph raised for a combatant with no time data.
TimeDatais 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 run20261018-083415.222), passes with it. Found by Peer Review (threadffbf1592). - Dead branches removed.
AddSortedTooltipData'spercent = 100for a zero total could not run, becausetotalstarts atEpsilon.SpellAuraDispelledStolen'sSPELL_DISPEL_FAILEDcheck could not run, because the dispatch table inTracker.luasends that event toSpellAuraDispelFailed.ThreatReturnerhad two identical returns. The mitigation module's roster handler checkedUnitGUIDbefore callingrescanUnit, which checks it (and secrecy) itself; the handler now leaves that torescanUnit. - A normal melee swing could be labelled as an extra attack. After
SPELL_EXTRA_ATTACKS(Windfury, Sword Specialization)Tracker.luatakes 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, threaddca25a1a, 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 toName <Owner>, so a pet's proc was never labelled (F1). Not yet verified in game. - A mode registered twice was listed twice.
Recount:AddModeTooltipappended 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 (thread6048156e, A3). Found by the duplicate-listing check added to the revision specs after Peer Review's F5 (threaddca25a1a).AddModeTooltipandRecount:SetupMainWindownow share onefindModelookup by label (thread6048156e, 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 anOptionalModeslist, and a removed mode is taken out withtable.removeinstead of being set to nil and closed up by a second loop.Recount:GetModeIndexmoved toRecount_Modes.luaand is now the one lookup by label for everything else:GUI_Main.lua'smodeIndexOf(the tab bar) is gone, andRecount:SetMainWindowModeByLabel(the key bindings) calls it. It tries the label as shown, then the label as an English locale key (thread88a80cd4, B1). The oldGetModeIndexmatched either in one pass, so a locale key whose translation was an earlier mode's shown label picked that mode.SetMainWindowModeByLabelreadL[label], which made AceLocale report a missing entry for any label that is not a locale key; it reads withrawgetnow, asGetModeIndexalready 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, andRecount:UpdateSummaryModereaddata.Petwith no record.Recount:DeleteCombatantnow hides the detail window when it shows the deleted combatant, andUpdateSummaryModereturns when there is no record. Found by Peer Review (thread88a80cd4, B3). - Hovering a plotted compare graph anchored its crosshair and tooltip to the wrong object. The graph's
OnUpdatehandler names its framegraph, but three calls still passedself, which insideRecount:CreateCompareWindowis the Recount addon object, not a frame: the twohairline:SetPointanchors andGameTooltip:SetOwner. They now passgraph. What the client did with the addon object as an anchor is not verified in game.GetCursorPositionis now cached as a local. - The compare graph window was English only. Every label, button, dropdown entry, status line and tooltip in
GUI_CompareGraph.luawas a literal; all 43 now go through the locale, with new enUS keys, solocale_specchecks them. Numbers in text use format keys (Fight %d (%ds),Showing %d series). Found by Peer Review (thread88a80cd4, 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. Itsor "OverallData"fallback is gone;CurDataSetis set at load. Found by Peer Review (thread88a80cd4, N1). - "Top 3" tooltips listed one row three times.
Recount:AddSortedTooltipDataloopedfor rank = 1, numbut readSortedData[i], whereiis the count of rows collected, so every line showed the last-sorted entry with numbers 1, 2 and 3. It now readsSortedData[rank]. Every mode's tooltip goes through it. - A grouped pet or totem whose owner was never seen lost all its numbers.
Recount:AddCombatantgave a grouped non-player with no known owner notype(the branch was commented out), andAddAmountdrops 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 typedUnknown, 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 valuetrue. The keys stay as they are, since every locale andBindings.xmluse them; the five enUS entries now have values spelledMaelstrom.GetModeIndexfalls back to the locale key, so the binding still selects the mode, and the short tab nameMaelfollows becauseSHORT_MODE_NAMESis keyed by locale key. Saved tabs hold the shown label, so a tab saved as "Maelstorm Gained" would have shown the initials fallbackMG(Mana Gained's short name) until clicked;Recount:LoadMainWindowDatanow 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 (threadd149f829, S4); the saved-tab half was found on this session's own audit.
Documentation
- README, CurseForge description and
docs/FEATURE_IMPROVEMENTS.mddescribe 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.xmlhas 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, anddocs/FEATURE_IMPROVEMENTS.mdrecords 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-onlyRecount: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.luaat 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 inRecount_Modes.lua,TrackerModule_PowerGains.luaandTrackerModule_Dispels.luais covered (desk coverage run20261017-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_specandmitigation_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 creditsName-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 run20261017-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 run20261017-085721.316-3048413046).combatlog_spec,modes_spec,mitigation_specandenv_recount: the answers to threadeac2bef3, 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'sretainDuring, and each reset callsclearLoaded()before reinstalling globals;retainDuringalone 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_specadds 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_specadds the Stoneskin label on a client whoseC_Spellhas noGetSpellInfo.Recount_Modes.luaand the Dispels, PowerGains, Interrupts, CCBreakers, Resurrection, Mitigation and Totems modules are at 100% line coverage (desk coverage run20261017-092334.638-1930624320); 1047 passed, 0 failed (desk run20261017-092721.191-2973030455).combatlog_specandmodes_spec: threaddca25a1a. 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 theAddModeTooltipfix). 1050 passed, 0 failed (desk run20261017-093848.205-2880153567).modes_spec: one guard, and the optional-mode list. The "nothing to show" sweep adds a combatant record with noFightstable to every mode's returner and tooltip; withRecount:CurrentFightreduced to the old one-part check it failed 57 times (desk run20261017-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_specopens the summary for a combatant with no row in the fight; it failed with the old unguarded read (desk run20261017-095842.977-2105790810). 1053 passed, 0 failed (desk run20261017-095718.924-3949104247).Recount_Modes.luaand the seven modules are at 100% line coverage, and the nil-GUID return inSpellExtraAttacksis covered (desk coverage run20261017-095316.243-2918266046).interactions_specandguidepth_spec: thread88a80cd4. On a simulated translated client,GetModeIndexandSetMainWindowModeByLabelfind 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 andL[label](desk run20261017-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 run20261017-101039.429-4089132674). Hovering a plotted compare graph shows the readout owned by the graph frame; it failed withselfput back (desk run20261017-101852.156-2856416235). 1060 passed, 0 failed (desk coverage run20261017-102156.695-3378063772, which runs the suite).Recount_Modes.luais at 100%. Every line changed inGUI_Main.lua,GUI_Detail.lua,GUI_TitanPanel.luaandGUI_CompareGraph.luais 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_specandinteractions_spec: threadd149f829. The five Maelstrom entries readMaelstromin English, and the old-spelling key, the binding'sSetMainWindowModeByLabelcall 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 withselfput back on the anchors and the summary's early return removed (desk run20261017-103050.517-2685381676).tabs_specrenames a saved "Maelstorm Gained" tab to "Maelstrom Gained" with short nameMaeland leaves a label no mode answers to alone; it failed with the rename disabled (desk run20261017-103859.468-3846036294), and the Maelstrom specs failed with the enUS value put back totrue(desk run20261017-103927.787-100410570). 1063 passed, 0 failed (desk run20261017-104039.426-2487061454).modes_spec: totems known by spell name. The fixture names Searing, Windfury and Fire Elemental Totem through the harness'swow.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 run20261017-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 run20261017-124439.880-501608816).TrackerModule_Totems.luaat 100% line coverage; 1067 passed, 0 failed (desk coverage run20261017-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 underFluffy <Testchar>, the name the fixture's combat log stored it by; a party member from another realm underName-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.EnableThreatRequirementsturns on every setting it names.modes_speclists Live Threat with the modes its fixture cannot feed, andtabs_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.luaat 100% line coverage; 1099 passed, 0 failed.interactions_spec,config_specandroster_spec: threadef07ad9f.EnableThreatRequirementsturns 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.UnitCombatantNamejoins 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 ofFindTargetedUnitandroster.lua's revision line are now driven. None of these was run against the code without the change.TrackerModule_Threat.luaandroster.luaat 100% line coverage; 1113 passed, 0 failed (desk coverage run20261017-160941.806-949774343, which runs the suite). The plain suite run twice hit the 60-second per-file limit inmodes_specwhile the CPU read 100% with other test runs going; with--timeout 300it passed.interactions_spec: stale bars, stale report rows and the bar scale (threads36c733ea,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 runs20261017-163400.745-1569536396and20261017-171728.449-2423574238). 1116 passed, 0 failed (desk run20261017-171533.327-3281139599, with--timeout 240; two default runs before it stoppedmodes_specat the 60-second per-file limit with the CPU at 99%).interactions_specandmodes_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 inme:RenderPanereproduced 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 run20261017-183921.639-3932648241,--timeout 240; a default run before it stoppedmodes_specat the 60-second limit with the CPU at 97%).interactions_spec: thread2af65df6. 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 isLive Threat: <target>and it sends no total with the total bar on. 1124 passed, 0 failed (desk run20261017-190650.005-12603724).interactions_spec: threadeaa2b2be. 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 run20261017-192049.446-2853367936); only the Ctrl+click path forced to open a realtime window (both Ctrl+click checks, desk run20261017-192305.211-347880913, which also settles that the spec's Ctrl key reachesGUI_Main's cachedIsControlKeyDownand that a realtime window is detectable); the Total-row gate, the report subject, the share rounding and theKThreatprecision (one spec each, desk run20261017-192516.338-1484307619); the report's total gate (desk run20261017-192721.730-3531921440).TrackerModule_Threat.luaat 100% line coverage;GUI_Main.luaat 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 run20261017-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 thatFight1still shares the one table; Writ's test-weakening guard had refused a plain 6.3 -> 6.294 swap there (thread7d5d14fc, closed). Whole suite 1132 passed, 0 failed (desk run20261017-225926.452-2918418133).interactions_spec: the Battle.net report and the rest of the report header (threadseaa2b2beC1,5dda0799B3). 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 throughC_BattleNet.SendWhisper, the functionGUI_Main.luaprefers, and once through theBNSendWhisperfallback with noC_BattleNet.SendWhisper. At harness943bc1ctheC_BattleNet.SendWhispervariant asserts on the harness's ownwow.bnWhispers, and passes aBNSendWhisperthat raises, so aGUI_Mainthat preferred the global fails it (seen to fail with line 29's order flipped). The fallback variant keeps its ownBNSendWhisper, because the harness's forwards to theC_BattleNet.SendWhisperthat variant hides. Every load resets the env, which rebuilds both, so nothing carries into a later load (threads7b76cf8b,e7912e7f). It first failed with the header's whisper branch sent to chat instead (desk run20261017-201043.560-2332766922). A Damage Taken report's total line is exactly0. Total 300 (100%), with no rate. A report on a numbered fight is headedRecount - 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%)and1. Testchar 500 (fast, 50%). Every line ofRecount:ReportDatais now covered;GUI_Main.luais at 83.42% (desk coverage run20261017-204238.927-3006797611, which ran the suite before the rescale assertion above was settled). The rest ofGUI_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.
LoadConfiginGUI_Config.luare-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 underlibs/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'srequired-dependenciesentry for the same slug, so every upload sent LibDBIcon as an embedded library. Every flavour but Forever lists LibDBIcon-1.0 in## Dependenciesand 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.luaCreateWindowModuleSelection). /recount guiopens a separate Ace3 options window (Recount.lua,AceConfigDialog:Open), not the WoW settings page.- keybindings cover most modes, not all:
Bindings.xmlhas 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.- 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 (
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 oldLoadConfignever touched the Threat box, which the assertion names.toc_spec: no tracked.luaunderlibs/carries an embed marker for a slug.pkgmetarequires. Run with the marker put back: it failed naminglibs/LibDBIcon-1.0/LibDBIcon-1.0.luaandlibdbicon-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
amountPerSecondis secret, and v1.19.4'sperSecondTexthanded a secret toAbbreviateNumbersunrounded, which returns anything under 1000 as it is (the plain path was already rounded). Addon code cannot round a secret, soperSecondArginTracker_Midnight.luanow returns a format spec with the value: a plain rate keeps"%s"with the rounded, abbreviated string, and a secret rate gets"%.0f", soFontString:SetFormattedTextdoes the rounding (SimpleFontStringAPIDocumentation.luadeclares itAllowedWhenTaintedin 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 reads5235rather than5.2Kuntil 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%)".
RefreshTestBarinGUI_Config.luaappended the literal string"397821.2", skipping the number format the total beside it already used. It now goes throughFormatLongNumstoo. 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'sOnClick. 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 reachesSetFormattedTextas the secret itself, under a%.0fspec, in DPS mode and in Damage Done with Per Second on. The harness models a secretSetFormattedTextas 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 calledSetText), 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'srenderRowwrote onlyAbbreviateNumbers(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 throughFontString:SetFormattedText, whichSimpleFontStringAPIDocumentation.lua(live tree) declaresSecretArguments = "AllowedWhenTainted", so the values are formatted by the client and Lua still never operates on a secret. A plain rate is rounded first (perSecondText), sinceAbbreviateNumbersleaves anything under 1000 unrounded. Percent is shown only while the source's amount and the session'stotalAmountare both readable (plainPercent, gated onissecretvalue, the ruleGUI_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.luaGetPercentageValue), 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; thatSetFormattedTextaccepts a secret value in combat is still from the documentation, not seen in game. deletion.lua: the scenario fallback inDetectInstanceChangeno longer overwriteszone, and its debug line cannot raise.C_Scenario.IsInScenarioreturns one boolean (every caller inwow-ui-source-classic_erareads only that; the generated API docs do not list it), soinInstance, zone = C_Scenario.IsInScenario()always setzoneto nil, and(inInstance or "nil") .. " : "raises "attempt to concatenate a boolean" whenever the player is in a scenario. It now assignsinInstanceonly and prints both throughtostring. The branch runs only whenGetInstanceInforeturns no zone name, so no player is known to have hit it. Found from a language-servercast-local-typewarning.
Documentation
- The README and the CurseForge description list every slash command. Five that exist in
Recount.consoleOptionswere missing from both:clamptoscreen,framestrata,maxfights,syncandverchk. 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 ofgui,reset,verChk,show,pauseandsyncare locale strings (Frenchshowisafficher, Koreanresetis초기화). - 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, fromRecount.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'sAddDataSeriesandAddFilledDataSeriesuse#instead oftable.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-fieldrather than a code change, so it stays upstream's code..luarc.jsondeclaresGetMouseFocus, which LibDropDown reads with aGetMouseFocifallback.
Testing
- Six existing bar-name assertions now expect the rank prefix (
forever_spec72,midnight_spec279 and 884,split_spec495, 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'sFontString:SetFormattedTextaccepts a secret argument (requested on theharnesschannel, delivered asf1b2743). The pin moved fromb1f9386toe17d14c, which includes it. A first move to181539bfailed 647 of 745 examples on the env's live-frame guard (12,925 frames, limit 12,000): the harness'shooksecurefuncbookkeeping from515873bkept every earlier copy of Recount alive throughTrackerModule_Threat.lua's hook on the addon table. Reported to the harness, which confirmed it by measurement and fixed it ina4ca42c. The stand-in staged inenv_recount.luain the meantime is deleted.deletion_spec: the scenario fallback with no zone name known printstrue : niland 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 byme:CreateTabWindowinGUI_Main.luathe first time it is opened) is only bars and a title, moved and sized on its own, with the main window's colours (Widgets.luanow styles it as one), scale, lock, strata and clamp. It is a third pane,Recount.MainWindow.Window, with aHostfield naming its window:PlaceRow,CreateRow,LayoutPane,RenderPaneand the Midnight renderer now size and parent a pane's bars through its host, andme:BindPane(formerly the lower pane'sBindLowerPane) binds either pane to its tab. It refreshes on its own one-second timer, so it keeps updating while the main window is hidden;RefreshMainWindowand the Midnight renderer now draw it whether or not the main window is shown. Saved per profile inMainWindow.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 afterInitOrderso 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 callFrame:RegisterEvent()fromOnEnable(Recount.lua,RegisterCombatLogEvent). Reported 2026-10-01 with no flavour named; the stack shows the restricted-client gate came out false, soCOMBAT_LOG_EVENT_UNFILTEREDwas registered on a client that forbids it. The gate tested onlyWOW_PROJECT_ID == WOW_PROJECT_MAINLINEplus the interface number, and v1.19.0's statement that Forever reportsWOW_PROJECT_MAINLINEwas never verified:WOW_PROJECT_IDis engine-set there and nothing inwow-ui-source-foreverassigns it for the_Camelotclient. The gate now also treats a client with no combat-log row reader as restricted: every Classic tree'sC_CombatLogdocumentsGetCurrentEventInfo, the live and Forever trees' do not, and their deprecatedCombatLogGetCurrentEventInfoforwards 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).UnitGUIDitself can return a secret on 12.0+, and the module used the GUID as a key intoactiveandguidToUnitwith only a nil check; the aura fields were guarded, the GUID was not.MitigationUnitAura,rescanUnitandMitigationRosterUpdatenow skip a unit whose GUID is secret (isPlain). Nothing is lost: on those clients the combat log is gone, soCreditMitigationis 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_UPDATEandPLAYER_ENTERING_WORLDonly whenRecount.IsMidnightClientis false: everyAddDamageDatacaller is a combat-log handler inTracker.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'sMODE_METRICmaps 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
.pkgmetacarries no comments. The header, the commented-outexternals: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 inMainWindow.Tabsby mode LABEL, not index, because indices move when a tracker module is switched on or off;LoadMainWindowDatanow 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 byFloatingChatFrame.xmlin 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 wheneverWindowOrder.luaraises 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 deliversOnMouseUpthere (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 isRecount.MainWindowitself (so every existing reader ofRows,DispTableSorted,GetDataand the rest is unchanged) orRecount.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 nowRenderPane, run once per pane;ResizeMainWindowlays 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-localMaxValueis 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, throughRecount:MainWindowLowerPane). Saved per profile inMainWindow.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.ResetDatanow clears every pane throughFullRefreshMainWindow; 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.UpdateBarTextColorsnow walks every created bar of both panes rather than0..RowsCreated, which skipped every bar once the total bar was switched on after the others existed (creating bar 0 setRowsCreatedto 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 ofSPELL_INTERRUPTran only housekeeping, so "who keeps interrupting me?" could not be answered.AddInterruptDatanow also addsInterruptedandInterruptedBy(interrupter -> interrupted cast) to the victim, as Dispels keepsDispelled/WhoDispelled, andInitFightDatasets 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 (inbox1ec972ac). 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 thelibs\LibDataBroker-1.1andlibs\LibDBIcon-1.0loads, and drop both fromX-Embeds;.pkgmetalistslibdbicon-1-0underrequired-dependencies, so the CurseForge app installs it. The standalone brings LibDataBroker-1.1 with it (its ownembeds.xml). A bundled copy only updated when Recount re-released; the standalone updates for every player.Recount_Camelot.tockeeps loading the bundled copies, because the standalone's TOC does not list Forever's Interface 16001, so the files stay underlibs/. 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'sgetAnchors), moved out ofGUI_Minimap.luaand 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
UnitIsTrivialis 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.luadoes 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:GetModeIndexno longer readsL[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 withrawget. Found while auditing the tabs. Nothing in the addon calls it (the keybindings useSetMainWindowModeByLabel, 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 readL[...], 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 newRecount: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 inGUI_Main.luastill 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. Asplit_specexample checks both panes empty and refill through it.
Development
- Language-server warnings cleared.
.luarc.jsondeclaresChatFrame1andTooltipBackdropTemplateMixin, which the bundled LibDropDown reads. LibDropDown's options builder no longer assignsdel()'s return value back tosortedOptsandlookup; it callsdeland then sets each tonil.delalready returnednil, so the behaviour is unchanged. wow-version-replication.ps1no 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-Filenow 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_speccompares the client's whole class-colour entry before and afterGetColor;toc_specrefuses any field spelled likeOptionalDeps, whatever its case;lazysync_specalso checks another player's version survives. toc_speccoversRecount_Camelot.toc, which it never read. Forever joins the per-flavour checks (interface 16001, same title, saved variables, noOptionalDeps), 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, andTests/env_recount.luahands back everything it now provides (inbox thread0fb44ef9, 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 wholeCOMBATLOG_OBJECT_*/SCHOOL_MASK_*table,FauxScrollFrame_*,MinimapCluster,GetNumDisplayChannels, and ourC_DamageMeter. Also gone, because nothing in Recount calls them:RegisterAddonMessagePrefix, the bareGetSpellName,GetMinimapShape(LibDBIcon falls back to "ROUND") andIsInScenarioGroup(deletion.luachecks for it). What stays is what the harness leaves out on purpose:UnitIsTrivial,YELLOW_FONT_COLOR, and the pre-SettingsInterfaceOptionsFramepair for the legacy-settings spec. Measured by resetting the harness modules withoutenv_recountand 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.setDamageMeterSessionnow writes the harness'swow.damageMetermodel, 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. OurC_DamageMeterhad 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_specsetswow.zone, because the harness'sGetInstanceInfonames the zone from it and an empty zone is Recount's "not loaded yet, retry" path;lazysync_specstubsUnitIsConnectedfor its stubbed raid, which Recount asks by player name. - 816 passed, 0 failed.
Recount.lualine coverage is 84.04%.
modes_speccovers "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 readsInterruptedBy. 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, becauseUIFrameFade(every client'sBlizzard_SharedXMLBase/FrameUtil.lua) was missing from the harness at870c83d; a yielding stand-in covered it until the harness shipped the whole family.- The harness pin moved
870c83d->27b84b3->1867896->6820bd5and theUIFrameFadestand-in is deleted (inboxa4066a79). At27b84b3a frame with no height and a single anchor has no rect, as in the client, andGameTooltiphad no height, so four tooltip-placement examples read nil;1867896sizes the tooltip from its lines (inboxf3ef297d) and they pass with no Recount-side workaround.6820bd5reports a spec file that hits the runner's time limit as a named TIMEOUT instead of crashing with(error object is not a string)(inbox4fb957ea). - The suite no longer keeps every earlier copy of Recount alive.
env.initializenow dropsRecount*globals whose frames came from an earlier reset (dropStaleNamedFrames), as a/reloadempties_G. A named frame only some specs create, such as the compare window'sRecountCompare_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, somodes_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_spectakes 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.initializenow 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_specgains four examples (21 in all). One checks thatRecount:ShowDataSetempties 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'swow.mouseButtonsDown(the harness pin moved6820bd5->b1f9386, which shipsIsMouseButtonDownat Recount's request, inbox7494ccfc; the stand-inenv_recountcarried 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 throughTimeTick, and with nobody in combat by the test roster each tick ends the fight after one second ofGetTime, which Recount discards as under three seconds (LeaveCombat->CopyCurrentFights). The fight is now fed without ticks, and every assertion is unchanged.locale_specchecks everyL["..."]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 ofAceLocale-3.0.lua:62-66and: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 asL[someVar], the shape of theGetModeIndexfix 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.
UpdateZoneGroupFiltercalled the bareGetZonePVPInfo, which Forever does not have; it ships onlyC_PvP.GetZonePVPInfo(wow-ui-source-forever,PvpInfoDocumentation.lua). The file-local now takesC_PvP.GetZonePVPInfowhen 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 inBlizzard_DeprecatedCombatLog, which returns early unless theloadDeprecationFallbacksCVar is on.Recount.lua'sFlagsToUnitIDuses them as table keys while the file loads, so with the CVar off it would raise "table index is nil";CreateOwnerFlagsandAddCombatantadd and mask them. Each now has anorliteral like the flags above it.- Chat reports:
SendChatMessageandBNSendWhisperare deprecation fallbacks on Forever too.GUI_Main.luaandGUI_Detail.luanow takeC_ChatInfo.SendChatMessage/C_BattleNet.SendWhisperwhere 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
GetMouseFocuswhen the pointer left a submenu entry. It is gone from Forever and from retail 11.0+; the library now falls back toGetMouseFoci()[1].
Development
wow-version-replication.ps1replicates 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
_Camelotclient, interface 16001).Recount_Camelot.tocloads the same file list as the Mainline TOC. Forever is the retail client --WOW_PROJECT_MAINLINE,COMBAT_LOG_EVENT_UNFILTEREDflaggedHasRestrictions, and the sameC_DamageMeteras Midnight (wow-ui-source-forever1.60.1.69977,CombatLogDocumentation.lua,DamageMeterDocumentation.lua) -- so it takes the Midnight data path. The Midnight gate inRecount.lua,GUI_Main.lua,Tracker_Midnight.luaandGUI_Detail_Midnight.luawasinterface >= 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, inRecount.lua, which publishesRecount.IsMidnightClientandRecount.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 threwADDON_ACTION_FORBIDDENon 12.0. Not yet run in a Forever client.
Fixed
- Forever: a click on a bar in combat would have errored.
DamageMeterCombatSource.sourceGUIDisConditionalSecreton Forever (live leaves it unmarked), and the spell-detail window passed it straight back intoC_DamageMeter.GetCombatSessionSourceFromType, whoseSecretArguments = "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.
IsInScenarioGroupis called only if the client has it. Nothing on this box says whether Forever keeps it, so the retail branch ofdeletion.luachecks rather than assumes.- Retail Midnight and Forever: the spell breakdown could error in combat on a hidden spell ID (audit finding 34).
DamageMeterCombatSpell.spellIDis not declaredNeverSecretin the client's schema, and the getter isSecretWhenInCombat, 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 itsisFullUpdatecheck 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_SetSelectedIDas a string. The two are separate now; a missing texture selects nothing. Every flavour. - Reset Colors wrote into Blizzard's own class-colour table.
ResetDefaultClassColorsseta = 1directly on theRAID_CLASS_COLORSentry (or a class-colour addon'sCUSTOM_CLASS_COLORSentry) 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:installGlobalsreplaced_G.Enumwholesale, which deleted the harness'sEnum.RegisterAddonMessagePrefixResultand failed 34 lazy-sync examples inside the harness'sC_ChatInfo. It now extendsEnum. - The private test environment now overrides nothing the harness defines. Three harness defects found along the way --
IsInGroupignoring its group category, andAbbreviateNumbersandFontString:SetTextrefusing a secret the client accepts -- were reported, wrapped locally for a few hours, fixed by the harness in2c5f7fc, and the wrappers deleted. The pin is nowff6e593, which also delivers harness item 12 (text width scales with font size); no example moved. Handed back: the stubs it overwrote, the privatebitlibrary,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 withoutC_SettingsUtil).env.time,env.locale,env.printed,env.chatMessagesandenv.openedCategoriesare 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'swow.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 (AbbreviateNumbersandFontString:SetText, bothAllowedWhenTainted); the harness fixed both in2c5f7fc. - Peer review and harness requests moved to writ's inbox.
docs/AUDIT.mdandTests/HARNESS_CONTRACT.mdwere imported into the inbox and deleted (135a238); their full text stays in git history, committed first in31e638d.
[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
ResetAllCombatSessionshalf ofC_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.isFullUpdatewas 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 ownIsMouseOvermethod, 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.82124328613on one row and5.2Kon 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
IsMouseOverfix, and it registers in a+2000version 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
90062and Recount at90068(both stock, both carrying the crash) and FastGuildInvite at91068(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. At92068this 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+0stock,+1000a private per-addon fork,+2000the shared copy.Only the
.luachanged: 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 theIsMouseOverfix landed -- the shared harness stubs the removedMouseIsOverglobal 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 theRegiontype 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 inTests/env_recount.luastays 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_eachwhen 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 = 0that 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 as162rather than161.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.widgetsCreatedByhelper inTests/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