modelframe: a unit pane paints, so its OnUpdateModel fires
The paint that fires OnUpdateModel is gated on [widget+0x318] != 0 (0x76d24c) — the pane's loaded model. This read that as "has a file path", and a SetUnit pane keeps no path at all: its subject resolves per render, app-side, so SetUnit writes `unit` and nils `path`. Every unit pane's OnUpdateModel was therefore permanently inert. Not cosmetic. unitscan's alert model is made visible *only* from inside its own OnUpdateModel: reset() sets alpha 0 and arms the handler with a ten-paint settle delay, and the eleventh call is what sets alpha back to 1 (unitscan.lua:268-292). With the handler never firing, the alert drew a correctly placed, correctly sized, fully transparent model and the horn played over an empty card. The layout probe that said the model frame was a healthy 200x120 was right; it was just invisible. A unit token that names nothing still fires nothing, which is the "!= 0" half of the gate. Nothing in stock FrameXML or pfUI is woken by this: probed, none of CharacterModelFrame, DressUpModel, TabardModel or PetModelFrame registers an OnUpdateModel at all, and the stock consumer (Cooldown.lua) is file-backed and was already firing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -316,6 +316,51 @@ fn on_update_model_fires_per_visible_paint_with_this() {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A pane whose subject is a **unit** paints too, so its `OnUpdateModel` fires. It keeps no file
|
||||
/// path at all — `SetUnit` nils it and the subject resolves per render — and the reference's gate
|
||||
/// is the loaded model (`[widget+0x318]`), not the path. unitscan's alert is the live consumer:
|
||||
/// its model is made visible only from inside that handler, so gating on the path alone drew a
|
||||
/// transparent model behind the horn.
|
||||
#[test]
|
||||
fn a_unit_panes_handler_fires_once_its_token_names_a_unit() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
seen = 0
|
||||
m = CreateFrame("PlayerModel", "U", UIParent)
|
||||
m:SetScript("OnUpdateModel", function() seen = seen + 1 end)
|
||||
m:SetUnit("target")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// No target: the token names nothing, which is the `≠ 0` half of the reference's gate.
|
||||
s.tick(0.1);
|
||||
assert_eq!(s.eval::<i64>("return seen").unwrap(), 0);
|
||||
|
||||
s.set_unit(
|
||||
"target",
|
||||
Some(crate::script::UnitState {
|
||||
name: Some("Timmy".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.tick(0.1);
|
||||
s.tick(0.1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return seen").unwrap(),
|
||||
2,
|
||||
"one per paint, as a file-backed pane gets"
|
||||
);
|
||||
|
||||
// `ClearModel` drops the unit with the path, so the pane stops painting.
|
||||
s.run("U:ClearModel()").unwrap();
|
||||
s.tick(0.1);
|
||||
assert_eq!(s.eval::<i64>("return seen").unwrap(), 2);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `0x719370` enqueues the completion before it tests the loop flag, so a loop's first pass fires
|
||||
/// `OnAnimFinished`; a pane with no file fails `0x76d24c`'s gate and never paints.
|
||||
#[test]
|
||||
|
||||
@@ -395,9 +395,19 @@ impl UiScript {
|
||||
.collect();
|
||||
model.on_update_model_frames = live;
|
||||
}
|
||||
// Only visible Model panes with a file set, resident or streaming: the paint that fires
|
||||
// the handler (`0x76d1bc`) is gated on `[widget+0x318] ≠ 0` (`0x76d24c`). On any other
|
||||
// kind the script is inert.
|
||||
// Only visible Model panes with a subject, resident or streaming: the paint that fires
|
||||
// the handler (`0x76d1bc`) is gated on `[widget+0x318] ≠ 0` (`0x76d24c`), which is the
|
||||
// pane's **loaded model**, not the file string.
|
||||
//
|
||||
// A `SetUnit` pane has one of those too, and keeps no path at all — its subject
|
||||
// resolves per render, app-side (`modelframe.rs`'s `SetUnit` writes `unit` and nils
|
||||
// `path`) — so gating on `path` alone left every unit pane's handler permanently
|
||||
// inert. Not cosmetic: unitscan's alert model is made visible *only* from inside its
|
||||
// own `OnUpdateModel` (`unitscan.lua:273-284` counts ten paints as a settle delay
|
||||
// after `reset()` has set alpha 0), so the alert drew a correctly placed, correctly
|
||||
// sized, fully transparent model and the horn played over an empty card. A unit token
|
||||
// that names nothing still fires nothing, which is the `≠ 0` half of the gate. On any
|
||||
// other kind the script is inert.
|
||||
let frames: Vec<FrameHandle> = model
|
||||
.on_update_model_frames
|
||||
.iter()
|
||||
@@ -405,7 +415,15 @@ impl UiScript {
|
||||
.filter(|&h| {
|
||||
model.arena.frame(h).is_some_and(|f| {
|
||||
f.effective_visible
|
||||
&& matches!(&f.kind_state, crate::widget::KindState::Model(m) if m.path.is_some())
|
||||
&& match &f.kind_state {
|
||||
crate::widget::KindState::Model(m) => {
|
||||
m.path.is_some()
|
||||
|| m.unit
|
||||
.as_deref()
|
||||
.is_some_and(|t| model.unit(t).is_some())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user