The slam macro, end to end through the path the game takes

He re-tested and it still never casts Slam, so I drove the whole thing rather
than the conditional: `UseAction` on a macro slot → `run_macro` fires
`EXECUTE_CHAT_LINE` per line (`0x4f14e0`) → the stock ChatFrame sends it through
its edit box (`ChatFrame.lua:1343`) → `ChatEdit_ParseText` finds
`SlashCmdList["CAST"]` → CleveRoids' hook → `GetParsedMsg` → `DoWithConditionals`
→ `Keywords.noslamclip` → `ValidateNoSlamClip` → `CastSpellByName`. On his own
interface, with his own 2.70 s weapon and Battle Stance, the line casts **Slam**
at the top of a swing and **Heroic Strike** 1.6 s in — which is the assertion the
old test never made, and the reason "the conditional answers true" meant nothing.

Three things came out of the drive.

**The load-time latch is worse than I wrote this morning, and it is what he is
seeing.** `GetSlamWindowPercent` reads SP_SwingTimer's `st_timerMax` cache, not
`UnitAttackSpeed`, so the `ADDON_LOADED` latch decides the window for the whole
first fight: the stand-in gives (2.0 - 1.5)/2.0 = **25%** against his weapon's
44%, while `st_timer` is still 0 so the swing reads **100% elapsed** — and
`[noslamclip]` is false with nothing said about it. Before the stand-in it was
false *and* printed "requires SP_SwingTimer" once; the fix traded a visible
failure for a silent one. Both states are pinned in the test now.

So the sheet is **seeded before the addon walk**, beside the roster record and
unit snapshot that are already seeded there for the same reason
(`ui_char::seed_combat_stats`). A `/reload` — and anyone mid-fight — now latches
the real weapon. A fresh login still has no self store (the create block has not
arrived, which is why the record comes off the roster), so the stand-in stands
there and heals on the first swing.

**And the stand-in now says so in the log.** One line per VM per token, through
the channel `/errors` lists: a combat-stats verb answering for "player" before
anything pushed that unit's sheet. One such read is expected at load; what the
line is for is the case where it never stops, which no test of the verb can see
because every one of them seats the sheet first. `UnitAttackSpeed` reaching Lua
with the store's own `UNIT_FIELD_BASEATTACKTIME` is now asserted through the real
`feed_char`, which is the test that was missing underneath all of this.

The dual-wield road is pinned too, because it is the one shape that can strand
the main-hand timer: `CheckDamageSource` returns "MAIN" on line one for a
two-hander and never runs its arithmetic, but dual-wielding it weighs the hit
against `UnitDamage`'s ranges scaled by the target's armour — and
`UnitArmor("target")` is 0 on any 1.12 client, ours included, because another
unit's resistances are not public. Every hit then reads UNKNOWN and falls to
`ClosestSwing == true or HeroicStrikeQueued()`, whose first half compares against
a *function* and whose second is true whenever this very macro has queued a
Heroic Strike — and `ResetTimer(true)` refills the **off-hand** timer while
`st_timer` runs to 0, where the elapsed read is 100 for good. That loop is the
addon's own and reproduces on any client; what we must not do is enter it when
the player is two-handed, which is `has_offhand` alone, and his combat log says
two-handed: 2.70, 2.71, 2.72, 2.68 between consecutive hits, with no 0.2 s
off-hand pairing anywhere.

189 failures, unchanged, no regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-29 17:33:31 -06:00
co-authored by Claude Opus 5
parent d89192d962
commit 3bbd99ebaf
6 changed files with 399 additions and 12 deletions
+45 -7
View File
@@ -427,14 +427,52 @@ fn with_unit_stats<T>(
token: &Option<String>,
f: impl FnOnce(&UnitCombatStats) -> T,
) -> T {
let model = lua.app_data_ref::<Model>().expect("model app_data");
let absent = UnitCombatStats::default();
let pushed = match token.as_deref() {
Some("player") => model.player_combat_stats.as_ref(),
Some("pet") => model.pet_combat_stats.as_ref(),
_ => None,
};
f(pushed.unwrap_or(&absent))
{
let model = lua.app_data_ref::<Model>().expect("model app_data");
let pushed = match token.as_deref() {
Some("player") => model.player_combat_stats.as_ref(),
Some("pet") => model.pet_combat_stats.as_ref(),
_ => None,
};
if let Some(stats) = pushed {
return f(stats);
}
}
// No sheet for this token: the answer is the stand-in, and for `"player"` that is worth one
// line in the log.
//
// Every addon that reads a swing speed, a damage range or an armour value divides by it, and
// several latch it: SP_SwingTimer caches `UnitAttackSpeed("player")` into `st_timerMax` at
// `ADDON_LOADED` behind an `if not st_timerMax` that a number satisfies, and CleveRoids
// computes its whole `[noslamclip]` window from that cache. A stand-in read there is wrong by
// the ratio of the real weapon to the vanilla 2000 — (2.0 - 1.5)/2.0 = 25% where the player's
// 2.70 s weapon gives 44% — and nothing anywhere raises. The window between a VM's birth and
// the first `feed_char` push always contains the addon walk, so one such read is *expected*;
// what this line is here to catch is the case where it never stops, which no test of the verb
// can see because every one of them seats the sheet first.
//
// Once per VM per token, through the same channel `/errors` lists: a per-call warning on a
// verb pfUI polls four times a second would be the log's whole content.
if let Some(token) = token.as_deref().filter(|t| *t == "player" || *t == "pet") {
let first = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.stats_stand_in_warned.insert(token.to_string())
};
if first {
crate::script::diagnostics::record_warning(
lua,
&format!(
"a combat-stats verb answered for \"{token}\" before anything pushed that \
unit's sheet, so it answered the stand-in ({} ms swing, zero damage). \
Expected once at load; if it is the last word for the session, `feed_char` \
never pushed and every addon dividing by a swing speed is wrong.",
VANILLA_SWING_MS
),
);
}
}
f(&absent)
}
/// The clamp `UnitDefense` (`0x519298`) and each hand of `UnitAttackBothHands` (`0x5188a7`) apply: