OnUpdate ran in frame-creation order, from a list built before the walk, so a frame hidden by an earlier handler still updated, and strata and level played no part. The reference's update walk `0x765650` goes through the nine strata from the bottom, each stratum's levels from 0, and each level's list oldest link first. It saves the next frame before each call, so a hide skips a frame before its turn. A frame linked into the level being walked waits a tick; one linked into a level still ahead runs there. The walk now follows that order, using the link stamps shows and level changes already keep. It checks each frame at its turn and places frames linked or given an `OnUpdate` mid-walk. An EditBox still flushes right after its own `OnUpdate`. Tests cover the order, mid-walk hides and shows, and a box's flush. Fixes #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
245 lines
10 KiB
Rust
245 lines
10 KiB
Rust
//! The host runtime loop: event fan-out ([`UiScript::fire_event`]), the per-frame advance
|
|
//! ([`UiScript::tick`]), which advances the `GetTime()` clock ([`UiScript::now`]).
|
|
|
|
use mlua::Lua;
|
|
|
|
use crate::widget::FrameHandle;
|
|
|
|
use super::{event, tooltip, Model, ScriptValue, UiScript};
|
|
|
|
impl UiScript {
|
|
/// Queue an event for the start of the next tick, behind everything queued earlier and before
|
|
/// that tick's `OnUpdate` pass. A drain runs a step behind its input, so firing at once would
|
|
/// overtake events that input already queued: the soulbind confirm for an item placed into a
|
|
/// worn slot must follow that placement's `CURSOR_UPDATE`, whose handler hides it
|
|
/// (`UIParent.lua:357-361`).
|
|
pub fn queue_event(&mut self, event: &str, args: Vec<ScriptValue>) {
|
|
self.model_mut()
|
|
.pending_events
|
|
.push((event.to_string(), args));
|
|
}
|
|
|
|
/// Fire an event to every frame registered for it, shown or not, setting both the
|
|
/// `this`/`event`/`arg1..argN` globals and the `(self, event, ...)` arguments (`0x704d50`,
|
|
/// `0x704f10`); handler errors go to [`UiScript::errors`]. Frames fire in registration order,
|
|
/// as `SignalEvent` (`0x703e50`) walks its tail-appended list (`0x7052d0`), and consumers rely
|
|
/// on it: both ZoneText frames write `PVPInfoTextString` on one event, and the last one wins.
|
|
///
|
|
/// The walk steps by the next listener saved before each handler runs (`0x703ee8`), so a
|
|
/// handler that unregisters itself does not skip its successor. Deviation: a handler that
|
|
/// unregisters the saved next ends the dispatch, because the reference then reads a freed node.
|
|
pub fn fire_event(&mut self, event: &str, args: Vec<ScriptValue>) {
|
|
fire_event_into(&self.lua, event, args);
|
|
}
|
|
}
|
|
|
|
/// [`UiScript::fire_event`] for a caller holding `&Lua`, such as a Lua binding.
|
|
pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
|
|
let model_mut = || lua.app_data_mut::<Model>().expect("model app_data set");
|
|
{
|
|
let mut at = {
|
|
let model = model_mut();
|
|
model
|
|
.event_to_frames
|
|
.get(event)
|
|
.and_then(|l| l.first().copied())
|
|
};
|
|
while let Some(h) = at {
|
|
let mut model = model_mut();
|
|
// A saved next that the previous handler unregistered ends the walk.
|
|
let Some(pos) = model
|
|
.event_to_frames
|
|
.get(event)
|
|
.and_then(|l| l.iter().position(|&x| x == h))
|
|
else {
|
|
break;
|
|
};
|
|
let next = model
|
|
.event_to_frames
|
|
.get(event)
|
|
.and_then(|l| l.get(pos + 1).copied());
|
|
let id = model.frame_id(h);
|
|
drop(model);
|
|
if let Err(e) = event::fire_event_handler(lua, id, event, &args) {
|
|
model_mut().record_lua_error(&e);
|
|
}
|
|
at = next;
|
|
}
|
|
}
|
|
event::fire_all_event_listeners(lua, event, &args);
|
|
}
|
|
|
|
impl super::UiScript {
|
|
/// Advance a frame: the `GetTime()` clock and queued events, then the update walk
|
|
/// (`0x765650`), each linked frame's update in strata, level and link order: its
|
|
/// `OnUpdate(self, elapsed)` (`0x704f10`), and for an EditBox its flush and caret blink after
|
|
/// it; then the engine's own fades, model panes and hover.
|
|
pub fn tick(&mut self, elapsed: f32) {
|
|
self.model_mut().now += f64::from(elapsed);
|
|
// Events queued since the last tick fire before this tick's OnUpdate.
|
|
let pending = std::mem::take(&mut self.model_mut().pending_events);
|
|
for (event, args) in pending {
|
|
self.fire_event(&event, args);
|
|
}
|
|
self.update_walk(elapsed);
|
|
self.tick_model_panes(elapsed);
|
|
// The engine's line fades, apart from any OnUpdate script: ScrollingMessageFrame's
|
|
// (`0x788460`) and MessageFrame's (`0x786200`), which also keeps only the lines that fit
|
|
// its height, so its rows are read from the resolved rect before the mutable walk.
|
|
let mut model = self.model_mut();
|
|
let ticked: Vec<FrameHandle> = model.arena.ticked_kinds().to_vec();
|
|
let message_frames: Vec<(FrameHandle, usize)> = ticked
|
|
.iter()
|
|
.copied()
|
|
.filter(|&h| {
|
|
model
|
|
.arena
|
|
.frame(h)
|
|
.is_some_and(|f| matches!(f.kind_state, crate::widget::KindState::Message(_)))
|
|
})
|
|
.map(|h| (h, Self::message_viewport_rows(&model, h)))
|
|
.collect();
|
|
for (h, viewport_rows) in message_frames {
|
|
if let Some(crate::widget::KindState::Message(mf)) =
|
|
model.arena.frame_mut(h).map(|f| &mut f.kind_state)
|
|
{
|
|
mf.tick(elapsed);
|
|
mf.trim_to_viewport(viewport_rows);
|
|
}
|
|
}
|
|
for &h in &ticked {
|
|
let Some(frame) = model.arena.frame_mut(h) else {
|
|
continue;
|
|
};
|
|
if let crate::widget::KindState::ScrollingMessage(smf) = &mut frame.kind_state {
|
|
smf.tick(elapsed);
|
|
}
|
|
}
|
|
drop(model);
|
|
// Tooltip fades: `FadeOut`'s ramp and the hide at its end.
|
|
tooltip::tick_fades(&self.lua);
|
|
// A frame hid or showed under a still cursor: re-run the hover walk at the saved position,
|
|
// as the reference's pump tail does (`0x765650` → `0x7660d0`). The hidden frame's OnLeave
|
|
// already fired, so only the new winner's `OnEnter` fires.
|
|
let repick = {
|
|
let mut model = self.model_mut();
|
|
let due = model.hover_repick;
|
|
model.hover_repick = false;
|
|
due.then_some(model.cursor_pos)
|
|
};
|
|
if let Some((x, y)) = repick {
|
|
self.mouse_move(x, y);
|
|
}
|
|
// `WOW_UI_HANDLERS=<secs>`: last, so its report covers everything this tick fired.
|
|
self.report_handler_profile(elapsed);
|
|
}
|
|
}
|
|
|
|
impl UiScript {
|
|
/// The model panes' per-frame pass, for visible panes, in the reference's order: the pane's
|
|
/// `OnUpdate` (`0x76d7f0`) advances its clock by `trunc(elapsed · 1000)` ms, the paint
|
|
/// (`0x76d1a0`) fires `OnUpdateModel`, and the animate fires `OnAnimFinished` (`0x76cdc0`) once
|
|
/// per arm when the armed sequence completes naturally, a loop's first pass included
|
|
/// (`0x719370`). A hidden pane's clock stands still. Completion is read after `OnUpdateModel`,
|
|
/// whose handler may re-arm.
|
|
fn tick_model_panes(&mut self, elapsed: f32) {
|
|
let dt_ms = (elapsed * 1000.0).trunc().max(0.0) as u64;
|
|
let update_ids: Vec<u32> = {
|
|
let mut model = self.model_mut();
|
|
let ticked: Vec<FrameHandle> = model.arena.ticked_kinds().to_vec();
|
|
for h in ticked {
|
|
let Some(f) = model.arena.frame_mut(h) else {
|
|
continue;
|
|
};
|
|
if !f.effective_visible {
|
|
continue;
|
|
}
|
|
if let crate::widget::KindState::Model(m) = &mut f.kind_state {
|
|
m.clock_ms += dt_ms;
|
|
}
|
|
}
|
|
// Kept by `SetScript`; a destroyed frame's handle compacts out on its first miss.
|
|
if model
|
|
.on_update_model_frames
|
|
.iter()
|
|
.any(|&h| model.arena.frame(h).is_none())
|
|
{
|
|
let arena = &model.arena;
|
|
let live: Vec<FrameHandle> = model
|
|
.on_update_model_frames
|
|
.iter()
|
|
.copied()
|
|
.filter(|&h| arena.frame(h).is_some())
|
|
.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.
|
|
let frames: Vec<FrameHandle> = model
|
|
.on_update_model_frames
|
|
.iter()
|
|
.copied()
|
|
.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())
|
|
})
|
|
})
|
|
.collect();
|
|
let mut ids: Vec<u32> = frames.into_iter().map(|h| model.frame_id(h)).collect();
|
|
ids.sort_unstable(); // creation order
|
|
ids
|
|
};
|
|
for id in update_ids {
|
|
if let Err(e) = event::fire_widget_handler(&self.lua, id, "OnUpdateModel", Vec::new()) {
|
|
self.push_error(e);
|
|
}
|
|
}
|
|
let finished_ids: Vec<u32> = {
|
|
let mut model = self.model_mut();
|
|
let ticked: Vec<FrameHandle> = model.arena.ticked_kinds().to_vec();
|
|
let mut due: Vec<FrameHandle> = Vec::new();
|
|
for h in ticked {
|
|
let Some(f) = model.arena.frame(h) else {
|
|
continue;
|
|
};
|
|
if !f.effective_visible {
|
|
continue;
|
|
}
|
|
let crate::widget::KindState::Model(m) = &f.kind_state else {
|
|
continue;
|
|
};
|
|
let Some(path) = m.path.as_deref() else {
|
|
continue;
|
|
};
|
|
let Some(facts) = model.model_facts.get(&crate::widget::model_key(path)) else {
|
|
continue;
|
|
};
|
|
if m.completion_due(facts) {
|
|
due.push(h);
|
|
}
|
|
}
|
|
let mut ids = Vec::with_capacity(due.len());
|
|
for h in due {
|
|
if let Some(crate::widget::KindState::Model(m)) =
|
|
model.arena.frame_mut(h).map(|f| &mut f.kind_state)
|
|
{
|
|
if let Some(a) = &mut m.armed {
|
|
a.finished = true;
|
|
}
|
|
}
|
|
ids.push(model.frame_id(h));
|
|
}
|
|
ids.sort_unstable();
|
|
ids
|
|
};
|
|
for id in finished_ids {
|
|
if let Err(e) = event::fire_widget_handler(&self.lua, id, "OnAnimFinished", Vec::new())
|
|
{
|
|
self.push_error(e);
|
|
}
|
|
}
|
|
}
|
|
}
|