From ebd533ee7d9e57b112e92576db3183f7408aaa72 Mon Sep 17 00:00:00 2001
From: ShempError <689+shemperror@noreply.octowow.st>
Date: Sun, 30 Aug 2026 23:23:45 +0200
Subject: [PATCH] TotemBar v0.4.1
---
CHANGELOG.md | 7 +++
README.md | 15 +++---
TotemBar.toc | 2 +-
core/cast.lua | 145 ++++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 143 insertions(+), 26 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b96861..1ac3a73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable changes to TotemBar are documented here.
+## v0.4.1 — 2026-08-31
+
+### Fixed
+- A totem destroyed by the enemy inside the ~0.4s window between a Totemic Recall wipe and its
+ refusal being attributed now stays dead instead of having its countdown resurrected by the
+ refused-recall restore (`core/cast.lua`).
+
## v0.4.0 — 2026-08-30
### Added
diff --git a/README.md b/README.md
index 237b114..56dc4ea 100644
--- a/README.md
+++ b/README.md
@@ -11,18 +11,17 @@ server data, not guesswork. No dependencies; adopts the
-## What's new — v0.4.0 · 2026-08-30
+## What's new — v0.4.1 · 2026-08-31
-- **Destroyed totems are detected.** A totem killed before its timer ran out used to keep
- counting down; now the death line (and, with SuperWoW/nampower, the cast and death events)
- clears its countdown, ring, pulse and range tint. Live retest still pending.
-- **No phantom countdown on a cast refused for mana.** The Clearcasting exemption never
- applied to totems and had silently disabled the mana gate.
-- **A refused Totemic Recall keeps your timers.** Only the Recall that actually went out clears
- them; a press the server rejects restores what it had cleared.
+- **A totem destroyed inside the recall attribution window stays dead.** A totem the enemy
+ destroyed in the ~0.4s window between a Totemic Recall wipe and its refusal being attributed
+ no longer has its countdown resurrected by the refused-recall restore.
**Version history** — details in [CHANGELOG.md](CHANGELOG.md):
+- **v0.4.1** (2026-08-31) — a totem destroyed by the enemy inside the recall attribution
+ window now stays dead instead of coming back when a refused Totemic Recall restores its
+ timers.
- **v0.4.0** (2026-08-30) — a totem destroyed before its timer ran out is now
detected and cleared (countdown, ring, pulse and range tint together) instead
of counting down as if it still stood; a totem cast refused for lack of mana
diff --git a/TotemBar.toc b/TotemBar.toc
index 00dd32c..c158cd5 100644
--- a/TotemBar.toc
+++ b/TotemBar.toc
@@ -2,7 +2,7 @@
## Title: TotemBar
## Notes: Shaman totem bar - pick one totem per element, cast it, or spam-cycle all four from a macro.
## Author: ShempError
-## Version: 0.4.0
+## Version: 0.4.1
## SavedVariables: TotemBarDB
core\totemdata.lua
diff --git a/core/cast.lua b/core/cast.lua
index 28022ca..bdc5e60 100644
--- a/core/cast.lua
+++ b/core/cast.lua
@@ -1307,6 +1307,21 @@ if CreateFrame then
TotemBar.TOTEM_ELEMENTS, name, owner, playerName)
if element then
TotemBar.evictDestroyedTotem(element)
+ return
+ end
+ -- Recall-attribution race (see "Recall-attribution vs.
+ -- destruction race" above): activeTotems has nothing for
+ -- this element because a recall wipe already cleared it and
+ -- is still awaiting attribution -- re-run the same owner/
+ -- name match against that pending snapshot so a later
+ -- restoreRecalledTotems doesn't bring the dead totem back.
+ local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
+ if snapshotRecs then
+ local snapElement = TotemBar.elementForOwnedTotemName(snapshotRecs,
+ TotemBar.TOTEM_ELEMENTS, name, owner, playerName)
+ if snapElement then
+ TotemBar.evictFromRecallSnapshot(snapElement)
+ end
end
return
end
@@ -1315,22 +1330,20 @@ if CreateFrame then
local element = TotemBar.elementForDiesLineCandidate(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, diesName)
if element then
- local rec = TotemBar.activeTotems[element]
- local exists, guid, health, deadOrGhost = nil, nil, nil, nil
- if type(UnitExists) == "function" then
- exists, guid = UnitExists(rec.guid)
- end
- if exists then
- if type(UnitHealth) == "function" then
- health = UnitHealth(rec.guid)
- end
- if type(UnitIsDeadOrGhost) == "function" then
- deadOrGhost = UnitIsDeadOrGhost(rec.guid)
- end
- end
- if TotemBar.totemDestroyed(exists, health, deadOrGhost) then
+ if TotemBar.verifyDiesLineDestruction(TotemBar.activeTotems[element]) then
TotemBar.evictDestroyedTotem(element)
end
+ return
+ end
+ -- Same recall-attribution race as the owned-name branch
+ -- above, for the generic " dies." fallback.
+ local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
+ if snapshotRecs then
+ local snapElement = TotemBar.elementForDiesLineCandidate(snapshotRecs,
+ TotemBar.TOTEM_ELEMENTS, diesName)
+ if snapElement and TotemBar.verifyDiesLineDestruction(snapshotRecs[snapElement]) then
+ TotemBar.evictFromRecallSnapshot(snapElement)
+ end
end
end
return
@@ -1346,6 +1359,7 @@ if CreateFrame then
-- events cannot cause a silent miss.
local element = TotemBar.elementForGuid(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual)
+ local unitName, ownerName, playerName = nil, nil, nil
if not element and arg1 and type(UnitName) == "function" then
-- GUID-free fallback (2026-08-20 event tap: UnitName/owner
-- DO resolve at the exact death instant on this client, for
@@ -1353,9 +1367,9 @@ if CreateFrame then
-- see the CHAT_MSG_COMBAT_FRIENDLY_DEATH/UNIT_DIED fixture
-- pair) -- covers a totem whose GUID was never latched by
-- either latch path at all.
- local unitName = UnitName(arg1)
- local ownerName = UnitName(arg1 .. "owner")
- local playerName = UnitName("player")
+ unitName = UnitName(arg1)
+ ownerName = UnitName(arg1 .. "owner")
+ playerName = UnitName("player")
element = TotemBar.elementForOwnedTotemName(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName)
end
@@ -1368,6 +1382,29 @@ if CreateFrame then
-- moment later" bug for the one case it exists to short-
-- circuit fastest.
TotemBar.evictDestroyedTotem(element)
+ return
+ end
+ -- Recall-attribution race (see "Recall-attribution vs.
+ -- destruction race" above): re-run both matches against the
+ -- pending recall snapshot, reusing whatever UnitName lookups
+ -- already ran above instead of re-querying the (by-now-gone)
+ -- unit a second time.
+ local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
+ if snapshotRecs then
+ local snapElement = TotemBar.elementForGuid(snapshotRecs,
+ TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual)
+ if not snapElement and arg1 and type(UnitName) == "function" then
+ if not unitName then
+ unitName = UnitName(arg1)
+ ownerName = UnitName(arg1 .. "owner")
+ playerName = UnitName("player")
+ end
+ snapElement = TotemBar.elementForOwnedTotemName(snapshotRecs,
+ TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName)
+ end
+ if snapElement then
+ TotemBar.evictFromRecallSnapshot(snapElement)
+ end
end
return
end
@@ -1514,6 +1551,80 @@ function TotemBar.restoreRecalledTotems()
return restored
end
+-- ===== Recall-attribution vs. destruction race =====
+--
+-- recallWipeActiveTotems above clears TotemBar.activeTotems immediately and
+-- keeps the pre-wipe records in TotemBar.lastRecallWipe.recs until the recall
+-- is attributed. If a totem that was JUST wiped dies server-side inside that
+-- same CAST_FAIL_WINDOW, the destruction handlers below (CHAT_MSG_COMBAT_
+-- FRIENDLY_DEATH / UNIT_DIED) look it up in TotemBar.activeTotems -- which is
+-- already empty for it -- so the eviction is a silent no-op and sets no
+-- tombstone. If the recall is THEN attributed as refused, restoreRecalledTotems
+-- puts the snapshot record back and the dead totem's countdown resumes. These
+-- two helpers let the destruction handlers reach into the still-pending
+-- snapshot with the exact same name/guid matches they already run against
+-- activeTotems, so a totem that died mid-attribution stays dead either way.
+
+-- The pending recall snapshot's records table, or nil if there is no recall
+-- wipe awaiting attribution right now (none at all, or it is already older
+-- than CAST_FAIL_WINDOW -- same staleness rule restoreRecalledTotems uses;
+-- an old snapshot means any subsequent event is unrelated to it).
+function TotemBar.pendingRecallSnapshotRecs()
+ local wiped = TotemBar.lastRecallWipe
+ if not wiped or not wiped.at or not wiped.recs then
+ return nil
+ end
+ if (GetTime() - wiped.at) > TotemBar.CAST_FAIL_WINDOW then
+ return nil
+ end
+ return wiped.recs
+end
+
+-- Evicts `element` from the pending recall snapshot (see above) instead of
+-- from TotemBar.activeTotems -- the wipe already cleared that table, so
+-- TotemBar.evictDestroyedTotem would no-op here. Removes the record from
+-- TotemBar.lastRecallWipe.recs so a later restoreRecalledTotems leaves this
+-- element alone, and tombstones it exactly like evictDestroyedTotem does, so
+-- pfUI's libtotem can't resurrect the display for it either.
+function TotemBar.evictFromRecallSnapshot(element)
+ local wiped = TotemBar.lastRecallWipe
+ if not wiped or not wiped.recs then
+ return
+ end
+ local rec = wiped.recs[element]
+ if not rec then
+ return
+ end
+ if rec.start and rec.duration then
+ TotemBar.destroyedTombstone[element] = rec.start + rec.duration
+ end
+ wiped.recs[element] = nil
+end
+
+-- Pure-ish (queries the live unit): runs the SAME "already exists, and either
+-- zero health or dead-or-ghost" verification the CHAT_MSG_COMBAT_FRIENDLY_
+-- DEATH generic " dies." fallback used inline before this was pulled
+-- out -- shared now so the recall-snapshot branch below runs the identical
+-- check instead of a second hand-copied one.
+function TotemBar.verifyDiesLineDestruction(rec)
+ if not rec then
+ return false
+ end
+ local exists, guid, health, deadOrGhost = nil, nil, nil, nil
+ if type(UnitExists) == "function" then
+ exists, guid = UnitExists(rec.guid)
+ end
+ if exists then
+ if type(UnitHealth) == "function" then
+ health = UnitHealth(rec.guid)
+ end
+ if type(UnitIsDeadOrGhost) == "function" then
+ deadOrGhost = UnitIsDeadOrGhost(rec.guid)
+ end
+ end
+ return TotemBar.totemDestroyed(exists, health, deadOrGhost)
+end
+
-- Is at least one totem currently out? Used to avoid wasting Totemic Recall's
-- own 6-second cooldown on a no-op cast: recalling with nothing out still puts
-- Recall on cooldown, so a fresh set placed right after can't be recalled for