Lua verbs fire their events before returning, in the reference's order (#1015)

In 1.12.1 an event a Lua verb raises runs its handlers before the verb returns: `SignalEvent` (`0x703e50`) walks the listeners in place. About 35 benilla binding sites queued theirs for a later drain instead.

- Verbs now raise inside `tick::verb` steps, and the events fire as each step's model borrow drops. A step follows the reference's split wherever it signals before changing state a getter reads: the money pickup, bag pickup and place, merchant grab, repair cursor, item use, action and pet-bar pickup and drop.
- The grid events fire before `CURSOR_UPDATE`, and a clear's unlock before both, as at `0x4952b3`/`0x495455`. `ITEM_LOCK_CHANGED` carries no arguments.
- `pending_events` is host-only now.

Tests pin handler order per family, nesting from a handler, and the order changes.

Fixes #999
Fixes #582

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 16:14:10 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 24c7cab983
commit aba120cdcb
26 changed files with 1042 additions and 593 deletions
+20 -18
View File
@@ -377,14 +377,13 @@ fn selected_row(model: &Model) -> Option<usize> {
.checked_add(1)
}
/// Queue `TRAINER_UPDATE`, which the reference fires from its mask commits, not from Lua:
/// `0x4d8c90` writes the filter mask, re-runs the finalizer and fires event `0x136`, and the
/// skill-line and expand-mask commits (`0x4d8cb0`, `0x4d8cd0`, the collapse verbs') do the same.
/// The stock window's filter click (`Blizzard_TrainerUI.lua:524`) does not repaint.
fn queue_trainer_update(model: &mut Model) {
model
.pending_events
.push(("TRAINER_UPDATE".to_string(), Vec::new()));
/// Raise `TRAINER_UPDATE`, as the reference's mask commits do after the re-filter: `0x4d8c90`
/// writes the type mask, re-runs the finalizer `0x4d8410` and signals at `0x4d8ca0`; the
/// skill-line and expand-mask commits (`0x4d8cb0`, `0x4d8cd0`, the collapse verbs') do the same
/// (`0x4d8cc0`, `0x4d8ce0`). The stock window's filter click (`Blizzard_TrainerUI.lua:524`) does
/// not repaint.
fn raise_trainer_update(model: &mut Model) {
model.raise("TRAINER_UPDATE", Vec::new());
}
fn set_collapsed(model: &mut Model, id: usize, collapse: bool) {
@@ -660,13 +659,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
g.set(
"SetTrainerServiceTypeFilter",
lua.create_function(|lua, (kind, on): (String, Value)| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
if let Some(c) = TrainerServiceCategory::from_filter_str(&kind) {
// The stock window passes 1 or 0; nil, false and 0 disable.
let enable = !matches!(on, Value::Nil | Value::Integer(0) | Value::Boolean(false));
model.trainer_filter[c.filter_slot()] = enable;
// Fires even when the bit did not move, as the reference's thunk does.
queue_trainer_update(&mut model);
super::tick::verb(lua, |model| {
model.trainer_filter[c.filter_slot()] = enable;
// Fires even when the bit did not move, as the reference's setter does.
raise_trainer_update(model);
});
}
Ok(())
})?,
@@ -677,18 +677,20 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
g.set(
"CollapseTrainerSkillLine",
lua.create_function(|lua, id: usize| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, id, true);
queue_trainer_update(&mut model);
super::tick::verb(lua, |model| {
set_collapsed(model, id, true);
raise_trainer_update(model);
});
Ok(())
})?,
)?;
g.set(
"ExpandTrainerSkillLine",
lua.create_function(|lua, id: usize| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, id, false);
queue_trainer_update(&mut model);
super::tick::verb(lua, |model| {
set_collapsed(model, id, false);
raise_trainer_update(model);
});
Ok(())
})?,
)?;
@@ -685,3 +685,19 @@ fn talent_order_sorts_on_state_within_a_group() {
let got: Vec<String> = visible(&mut s).into_iter().map(|(n, _)| n).collect();
assert_eq!(got, ["Riding", "Aye", "Bee", "Cee"]);
}
/// The mask commits fire `TRAINER_UPDATE` inside the verb, after the re-filter (`0x4d8410`, then
/// `0x4d8ca0`/`0x4d8ce0`): a handler reads the filtered tree before the next Lua line runs.
#[test]
fn the_filter_and_collapse_verbs_fire_trainer_update_in_place() {
let mut s = UiScript::new().unwrap();
s.set_trainer(Some(trainer()));
crate::script::tick::heard::listen(&s, &["TRAINER_UPDATE"], Some("GetNumTrainerServices()"));
let seen: Vec<String> = s
.eval(
"SetTrainerServiceTypeFilter('used', 0) local h = HEARD HEARD = {} \
CollapseTrainerSkillLine(1) table.insert(h, HEARD[1]) return h",
)
.unwrap();
assert_eq!(seen, ["TRAINER_UPDATE 4", "TRAINER_UPDATE 3"]);
}