casting a hunter shot starts Auto Shot, and the ping wears its real art

- Serpent Sting starts Auto Shot in play: the chain built earlier the same day never fired because the in-flight record was armed only for the cast classes it refuses on, and every hunter shot is a ranged attack — the reference records every commit and discriminates at the gate; two tests now run the real ladder and reproduce the shipped symptom
- the minimap ping is MinimapPing.mdx, measured: three additive quads — a 12 px spinner on the model's global clock, a static core, and a ring expanding 3→32 px on an 833 ms loop — the interim art was ~3× too big and missing the ring the eye actually reads
- a mouse release goes to the frame that took the press and nothing else — the reference's up handler reads the capture alone and never the hover frame, so a press over open space delivers no release to whatever the cursor wandered onto (corrects the morning's fallback)
- `WOW_MOVE_TRACE_TAGS=use` traces a right-click end to end (the cursor decision, the use packet, the spell-go answer), and the first headless hover capture parks the cursor on a chat tab so the reveal can be measured against the bare scene

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-25 18:50:40 +07:00
co-authored by Claude Fable 5
parent f8421c1545
commit 73826ca3d7
15 changed files with 753 additions and 116 deletions
+27 -19
View File
@@ -546,24 +546,22 @@ impl UiScript {
// ── `OnMouseUp` goes to the frame that took the PRESS, not to whatever is under the
// cursor now — the mouse CAPTURE (`root+0x80`).
//
// The client's mouse-DOWN handler `0x7662c0` "resolves target = root+0x80 (existing
// capture) else root+0x7c (hover frame) … then title-region drag or **capture**+OnMouseDown"
// (wow-re ledger `0x7662c0`, VERIFIED), so a press takes the pointer.
// The client's mouse-DOWN handler `0x7662c0` takes the pointer: "resolves target =
// root+0x80 (existing capture) else root+0x7c (hover frame) … then title-region drag or
// **capture**+OnMouseDown" (wow-re ledger `0x7662c0`).
//
// ⚠ **The release half is INFERRED, not carved.** That the capture is what the *UP*
// handler dispatches through is an extrapolation from the DOWN handler: the sibling
// `0x766420` (category `0xe`) carries only a bulk wave-classify label in wow-re's ledger,
// not a derivation. The inference is strong — a capture nothing consults on release would
// be pointless, and something has to clear `root+0x80` — but it is an inference, and this
// is a whole-UI change (every press-on-A-release-on-B in the client now delivers to A). A
// §5 is dispatched for `0x766420`'s target resolution (decision 1594); when it lands this
// comment says VERIFIED or this code changes.
// The **release** half is `0x766420`, and it is VERIFIED — wow-re
// `system/ui/scratch/mouseup-dispatch-law.md`, a §5 dispatched from this repo for exactly
// this line after it first landed on an extrapolation (decision 1599). It reads the capture
// and **nothing else**: `[mgr+0x80]` is snapshotted into `ebx` at entry (`0x76642b`),
// before anything, and `[mgr+0x7c]` — the hover frame — is **never read** anywhere in
// `[0x766420, 0x7664f0)`. One dispatch, `0x7664a4 call [eax+0x6c]`.
//
// We already track exactly the capture
// — [`Model::mouse_down_on`], the press target per button, which `OnClick`'s same-frame
// rule reads — and firing `OnMouseUp` at the *hit* frame instead was the one place it went
// unused. This function's own doc has always said "on the captured frame"; the code did
// not.
// So a press on A released over B fires `OnMouseUp` on **A**, and B gets nothing on the
// release edge. We already track exactly that capture — [`Model::mouse_down_on`], the press
// target per button, which `OnClick`'s same-frame rule reads — and firing `OnMouseUp` at
// the *hit* frame was the one place it went unused. This function's doc always said "on the
// captured frame"; the code did not.
//
// It is not a nicety: a chat window's resize grip is a 16×16 button that the drag pulls
// out from under the cursor the moment the size clamps at `SetMinResize`/`SetMaxResize`
@@ -572,10 +570,20 @@ impl UiScript {
// rest of the session — the stuck-drag failure `cursor::abandon_drag` exists to prevent on
// the other path.
//
// A press that captured NOTHING (over the world, or swallowed by a title region) falls
// back to the hit frame, which is `root+0x7c`'s arm of the same resolve.
// **A press that captured NOTHING fires nothing**, and this is the half the first version
// of this code got wrong: it fell back to the frame under the cursor, reading the DOWN
// handler's `+0x80`-else-`+0x7c` precedence as if the UP handler shared it. It does not —
// `0x766498 test ebx,ebx / je 0x7664cd` returns with no virtual call at all. The fallback
// was a divergence that let a press over open space deliver a release to whatever the
// cursor had wandered onto.
//
// (Two things the same §5 settled that are invisible from here: the capture is cleared at
// `0x7664bb`, **after** the dispatch — so a C++ consumer inside `OnMouseUp` still sees it,
// though no Lua binding reads it, `GetMouseFocus 0x48df40` being `+0x7c`; and the clear is
// gated on `evt+0x18 == 0`, the button mask *after* the event, so a **chorded** release
// fires `OnMouseUp` and keeps the capture for the button still held.)
let script = if down { "OnMouseDown" } else { "OnMouseUp" };
if let Some(id) = if down { hit_id } else { captured_id.or(hit_id) } {
if let Some(id) = if down { hit_id } else { captured_id } {
if let Err(e) = event::fire_widget_handler(&self.lua, id, script, vec![btn.clone()]) {
self.push_error(e);
}
@@ -718,3 +718,100 @@ fn the_repick_re_hovers_without_a_mouse_move() {
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// The mouse-UP dispatch law (decision 1599) — byte-verified, wow-re `mouseup-dispatch-law.md`
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// Two side-by-side frames, each recording every `OnMouseDown`/`OnMouseUp` it gets.
fn two_frames() -> crate::script::UiScript {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
log = {}
local function mk(name, x)
local f = CreateFrame("Frame", name)
f:SetPoint("BOTTOMLEFT", x, 200); f:SetSize(200, 200); f:EnableMouse(true)
f:SetScript("OnMouseDown", function(self) table.insert(log, self:GetName()..":down") end)
f:SetScript("OnMouseUp", function(self) table.insert(log, self:GetName()..":up") end)
return f
end
mk("A", 100)
mk("B", 400)
"#,
)
.unwrap();
s.resolve();
s
}
fn log_of(s: &mut crate::script::UiScript) -> String {
s.eval::<String>("return table.concat(log, ' ')").unwrap()
}
/// **The release goes to the frame that took the press, wherever the cursor has got to.** The
/// client's `0x766420` snapshots `[mgr+0x80]` — the capture — at entry (`0x76642b`) and dispatches
/// through it alone; `[mgr+0x7c]`, the hover frame, is never read in the whole function. So B, which
/// the cursor is over at release, gets nothing on that edge.
///
/// The concrete thing this buys: a chat window's 16×16 resize grip is pulled out from under the
/// cursor the instant the size clamps at a resize bound, and its `OnMouseUp → FCF_StopResize` has
/// to run anyway or the sizing drag is held for the rest of the session.
#[test]
fn the_release_goes_to_the_frame_that_took_the_press() {
let mut s = two_frames();
s.mouse_button(200.0, 300.0, "LeftButton", true); // press on A
s.mouse_move(500.0, 300.0); // ... cursor wanders onto B
s.mouse_button(500.0, 300.0, "LeftButton", false); // ... and releases there
assert_eq!(
log_of(&mut s),
"A:down A:up",
"the capture takes the release; B gets nothing on that edge"
);
}
/// **A press that captured nothing fires nothing on release** — and this is the half that is easy
/// to get wrong, because the DOWN handler *does* have a hover fallback (`root+0x80` else
/// `root+0x7c`) and the UP handler does **not** share it: `0x766498 test ebx,ebx / je 0x7664cd`
/// returns with no virtual call at all.
///
/// This shipped briefly as `captured.or(hit)` — a press over open space then delivered its release
/// to whatever the cursor had wandered onto, which is a frame that never saw the press.
#[test]
fn a_press_that_captured_nothing_delivers_no_release() {
let mut s = two_frames();
s.mouse_button(50.0, 50.0, "LeftButton", true); // press over open space
s.mouse_move(500.0, 300.0); // ... cursor onto B
s.mouse_button(500.0, 300.0, "LeftButton", false);
assert_eq!(
log_of(&mut s),
"",
"no capture, no dispatch — the UP handler has no hover arm"
);
}
/// The ordinary case still behaves: press and release on the same frame is down-then-up on it.
#[test]
fn a_press_and_release_on_one_frame_is_unchanged() {
let mut s = two_frames();
s.mouse_button(200.0, 300.0, "LeftButton", true);
s.mouse_button(200.0, 300.0, "LeftButton", false);
assert_eq!(log_of(&mut s), "A:down A:up");
}
/// The capture is **per button**, so a right-button release cannot be answered by a left-button
/// press's capture — `Model::mouse_down_on` is keyed by button name, which is what makes this fall
/// out rather than needing its own arm.
#[test]
fn the_capture_is_per_button() {
let mut s = two_frames();
s.mouse_button(200.0, 300.0, "LeftButton", true); // A holds the LEFT button
s.mouse_move(500.0, 300.0);
s.mouse_button(500.0, 300.0, "RightButton", false); // a RIGHT release, never pressed
assert_eq!(
log_of(&mut s),
"A:down",
"the right button captured nothing, so its release dispatches nothing"
);
}