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:
@@ -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:
|
||||
|
||||
@@ -1449,6 +1449,9 @@ pub(crate) struct Model {
|
||||
pub(crate) player_combat_stats: Option<char_stats::UnitCombatStats>,
|
||||
/// The pet's combat stats, which the reference's pet sheet reads as `UnitStat("pet", …)`.
|
||||
pub(crate) pet_combat_stats: Option<char_stats::UnitCombatStats>,
|
||||
/// The tokens a combat-stats verb has already answered the stand-in for, so the warning it
|
||||
/// records is one line per VM rather than one per call (`char_stats::with_unit_stats`).
|
||||
pub(crate) stats_stand_in_warned: std::collections::HashSet<String>,
|
||||
pub(crate) inventory_slots: char_stats::InventorySlots,
|
||||
/// The six bank-bag slots, ids 64..=69, fed from the player descriptor's guids.
|
||||
pub(crate) bank_bag_slots: char_stats::BankBagSlots,
|
||||
@@ -2343,6 +2346,7 @@ impl Model {
|
||||
.collect(),
|
||||
player_combat_stats: None,
|
||||
pet_combat_stats: None,
|
||||
stats_stand_in_warned: Default::default(),
|
||||
inventory_slots: Default::default(),
|
||||
bank_bag_slots: Default::default(),
|
||||
inventory_alerts: [0; 12],
|
||||
|
||||
Reference in New Issue
Block a user