The tooltip fade, a model's animation clock and its OnAnimFinished ran after the whole OnUpdate walk, and OnUpdateModel fired in creation order. A tooltip hidden through its parent also kept its owner and fade. In the reference, the walk makes one call per frame: its OnUpdate, then its kind's engine update (a tooltip's fade step, a model pane's clock and completion test). OnUpdateModel fires later, from the paint, in draw order. A tooltip's own hide and show run from any caller, its parent's cascade included: the hide un-owns and clears it; the show needs an owner. The walk now runs a per-kind update in each frame's slot, OnUpdateModel fires per stratum in draw order after the hover re-pick, and show and hide dispatch to a kind's override. CharacterModelBase's override is left for #890. Tests pin each order and a parent's hide and show. Fixes #861 Fixes #863 Fixes #864 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
8.6 KiB
Rust
247 lines
8.6 KiB
Rust
//! The engine's own per-kind updates, each run in its frame's slot of the update walk (`0x765650`,
|
|
//! `vtable+0x38` at `0x765710`) right after that frame's `OnUpdate`: GameTooltip's fade
|
|
//! (`0x530b20`) and a Model pane's clock and completion (`0x76d7f0`). `OnUpdateModel` fires later,
|
|
//! from the draw pass (`0x7657d0`), in draw order.
|
|
|
|
use crate::script::UiScript;
|
|
use crate::widget::{ModelFileFacts, SequenceFacts};
|
|
|
|
use super::common::script;
|
|
|
|
/// `LOG` joined by spaces, then emptied.
|
|
fn take_log(s: &UiScript) -> String {
|
|
s.eval::<String>("local l = table.concat(LOG, ' ') LOG = {} return l")
|
|
.unwrap()
|
|
}
|
|
|
|
/// `UI-Cooldown-Indicator.m2`'s two clamped 1000 ms sequences.
|
|
const COOLDOWN_FILE: &str = r"Interface\Cooldown\UI-Cooldown-Indicator.mdx";
|
|
fn cooldown_facts() -> ModelFileFacts {
|
|
ModelFileFacts {
|
|
sequences: [(0, 1000), (1, 1000)]
|
|
.into_iter()
|
|
.map(|(anim_id, duration_ms)| SequenceFacts {
|
|
anim_id,
|
|
duration_ms,
|
|
looping: false,
|
|
})
|
|
.collect(),
|
|
bbox: ([0.0; 3], [0.0; 3]),
|
|
cameras: 0,
|
|
}
|
|
}
|
|
|
|
fn lua_path(path: &str) -> String {
|
|
path.replace('\\', "\\\\")
|
|
}
|
|
|
|
/// A fading tooltip, owned and with a line, whose `OnHide` logs `hide`.
|
|
const FADING_TIP: &str = r#"
|
|
LOG = {}
|
|
local root = CreateFrame("Frame", "FadeRoot")
|
|
local owner = CreateFrame("Button", "FadeOwner", root)
|
|
owner:SetPoint("CENTER", 0, 0); owner:SetWidth(10); owner:SetHeight(10)
|
|
local tt = CreateFrame("GameTooltip", "FadeTip", root)
|
|
tt:SetFrameStrata("HIGH")
|
|
tt:SetOwner(owner, "ANCHOR_RIGHT")
|
|
tt:SetText("Fading")
|
|
tt:SetScript("OnHide", function() table.insert(LOG, "hide") end)
|
|
"#;
|
|
|
|
/// The fade is the tooltip's own update (`0x530b9c`, after its `OnUpdate` at `0x530b2d`): a frame
|
|
/// walked after it reads this frame's alpha, and on the frame the fade ends the hide (`0x530be9`)
|
|
/// and its `OnHide` come before that frame's `OnUpdate`.
|
|
#[test]
|
|
fn a_frame_walked_after_the_tooltip_sees_this_frames_fade() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.run(FADING_TIP).unwrap();
|
|
s.run(
|
|
r#"
|
|
local w = CreateFrame("Frame", "FadeWatch")
|
|
w:SetFrameStrata("TOOLTIP")
|
|
w:SetScript("OnUpdate", function()
|
|
if FadeTip:IsShown() then
|
|
table.insert(LOG, string.format("shown@%.1f", FadeTip:GetAlpha()))
|
|
else
|
|
table.insert(LOG, "hidden")
|
|
end
|
|
end)
|
|
FadeTip:FadeOut()
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.3);
|
|
assert_eq!(take_log(&s), "shown@0.4", "this frame's ramp, not the last");
|
|
s.tick(0.3);
|
|
assert_eq!(
|
|
take_log(&s),
|
|
"hide hidden",
|
|
"the hide lands in the tooltip's slot"
|
|
);
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// A pane's update (`0x76d7f0`) runs its `OnUpdate`, then advances its clock, whose completion
|
|
/// test drains `OnAnimFinished` in the same call (`0x7074d8`, `0x7075af`): in the pane's place,
|
|
/// before frames walked after it and before the draw pass fires `OnUpdateModel`.
|
|
#[test]
|
|
fn on_anim_finished_fires_in_the_panes_walk_slot() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
|
|
s.run(&format!(
|
|
r#"
|
|
LOG = {{}}
|
|
local p = CreateFrame("Model", "SlotPane")
|
|
p:SetFrameStrata("LOW")
|
|
p:SetModel("{}")
|
|
p:SetSequence(1)
|
|
p:SetScript("OnUpdate", function() table.insert(LOG, "pane") end)
|
|
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
|
|
p:SetScript("OnUpdateModel", function() table.insert(LOG, "model") end)
|
|
local h = CreateFrame("Frame", "SlotHigh")
|
|
h:SetFrameStrata("HIGH")
|
|
h:SetScript("OnUpdate", function() table.insert(LOG, "high") end)
|
|
"#,
|
|
lua_path(COOLDOWN_FILE)
|
|
))
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.6);
|
|
assert_eq!(take_log(&s), "pane high model");
|
|
s.tick(0.6); // 1200 ms ≥ the 1000 ms sequence
|
|
assert_eq!(take_log(&s), "pane finished high model");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// One virtual call runs both halves (`0x76d7fa`, then `0x76d843`): a pane that hides itself in
|
|
/// its `OnUpdate` still advances this frame, and a completion its advance reaches still fires.
|
|
/// A tooltip shows nothing here: its hide disarms the fade it would step (`0x530002`).
|
|
#[test]
|
|
fn a_pane_hidden_by_its_own_on_update_still_advances() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
|
|
s.run(&format!(
|
|
r#"
|
|
LOG = {{}}
|
|
local p = CreateFrame("Model", "SelfHide")
|
|
p:SetModel("{}")
|
|
p:SetSequence(1)
|
|
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
|
|
"#,
|
|
lua_path(COOLDOWN_FILE)
|
|
))
|
|
.unwrap();
|
|
s.tick(0.6);
|
|
s.run(r#"SelfHide:SetScript("OnUpdate", function() this:Hide() end)"#)
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.6);
|
|
assert!(!s.frame_visible("SelfHide"));
|
|
assert_eq!(super::model_clock::pane(&s, "SelfHide").clock_ms, 1200);
|
|
assert_eq!(take_log(&s), "finished");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// A zero advance returns before the completion test and the drain (`0x7074b7`): a sequence
|
|
/// already past its end fires nothing until a frame advances the clock.
|
|
#[test]
|
|
fn a_zero_advance_tests_no_completion() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
|
|
s.run(&format!(
|
|
r#"
|
|
LOG = {{}}
|
|
local p = CreateFrame("Model", "ZeroDt")
|
|
p:SetModel("{}")
|
|
p:SetSequenceTime(1, 1000)
|
|
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
|
|
"#,
|
|
lua_path(COOLDOWN_FILE)
|
|
))
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.0009); // trunc(0.9 ms) = 0
|
|
assert_eq!(super::model_clock::pane(&s, "ZeroDt").clock_ms, 0);
|
|
assert_eq!(take_log(&s), "", "no advance, no test");
|
|
s.tick(0.016);
|
|
assert_eq!(take_log(&s), "finished");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// Completion is judged against the arm the previous paint left: an `OnUpdateModel` that scrubs
|
|
/// the sequence back each paint does not hide the completion the walk already reported.
|
|
#[test]
|
|
fn completion_is_read_before_the_paint_re_arms() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
|
|
s.run(&format!(
|
|
r#"
|
|
LOG = {{}}
|
|
local p = CreateFrame("Model", "Scrub")
|
|
p:SetModel("{}")
|
|
p:SetSequence(1)
|
|
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
|
|
"#,
|
|
lua_path(COOLDOWN_FILE)
|
|
))
|
|
.unwrap();
|
|
s.tick(0.6);
|
|
s.run(r#"Scrub:SetScript("OnUpdateModel", function() this:SetSequenceTime(1, 0) end)"#)
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.6); // the arm from tick one ends at 1000 ms; this tick's clock reads 1200
|
|
assert_eq!(take_log(&s), "finished");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// `OnUpdateModel` fires from the paint (`0x76d1bc`), in the draw pass's order: stratum from 0
|
|
/// (`0x7657ff`), level from 0 (`0x7658c0`), a level's frames first linked first (`0x765920`).
|
|
#[test]
|
|
fn on_update_model_fires_in_draw_order() {
|
|
let mut s = script();
|
|
s.set_screen_size(800.0, 600.0);
|
|
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
|
|
s.run(&format!(
|
|
r#"
|
|
LOG = {{}}
|
|
function Pane(name, tag, strata, level)
|
|
local p = CreateFrame("Model", name)
|
|
p:SetFrameStrata(strata)
|
|
if level then p:SetFrameLevel(level) end
|
|
p:SetModel("{}")
|
|
p:SetScript("OnUpdateModel", function() table.insert(LOG, tag) end)
|
|
return p
|
|
end
|
|
Pane("PaneDialog", "dialog", "DIALOG")
|
|
Pane("PaneLow", "low", "LOW")
|
|
Pane("PaneLevel9", "level9", "MEDIUM", 9)
|
|
Pane("PaneLevel2", "level2", "MEDIUM", 2)
|
|
"#,
|
|
lua_path(COOLDOWN_FILE)
|
|
))
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.016);
|
|
assert_eq!(take_log(&s), "low level2 level9 dialog");
|
|
|
|
// Within a level the latest link draws last: a pane shown again goes after its peers.
|
|
s.run(
|
|
r#"
|
|
Pane("LinkA", "a", "BACKGROUND", 3)
|
|
Pane("LinkB", "b", "BACKGROUND", 3)
|
|
LinkA:Hide() LinkA:Show()
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
take_log(&s);
|
|
s.tick(0.016);
|
|
assert_eq!(take_log(&s), "b a low level2 level9 dialog");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|