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
+31 -18
View File
@@ -200,7 +200,7 @@ impl super::UiScript {
model.pending_wrap = Some(PendingWrap { bag, slot });
model.ui_cursor = Some(UiCursorMode::Cast);
model.ui_cursor_dirty = true;
cursor::queue_lock_changed(&mut model, bag, slot);
cursor::raise_lock_changed(&mut model);
}
/// The armed gift wrap, if any; the app reads it to hold the cursor's base at Cast.
@@ -293,7 +293,7 @@ fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) {
model.container_wraps.push((wrap.bag, wrap.slot, bag, slot));
// The paper unlocks and the cursor resets right after the send (`0x5eded0` at
// `0x5ee0aa`), not on the answer; the target never locks.
cursor::queue_lock_changed(model, wrap.bag, wrap.slot);
cursor::raise_lock_changed(model);
model.ui_cursor = None;
model.ui_cursor_dirty = true;
return;
@@ -339,13 +339,14 @@ fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) {
});
if let Some(item) = picked {
model.cursor = Some(CursorPayload::Item(item));
cursor::queue_cursor_update(model);
cursor::queue_lock_changed(model, bag, slot);
cursor::raise_cursor_update(model);
cursor::raise_lock_changed(model);
}
}
// The cancel is `ClearCursor(1,1)` (`0x4f9e0e`): the unlock, then `CURSOR_UPDATE`.
Some(CursorPayload::Item(held)) if held.bag == bag && held.slot == slot => {
cursor::queue_cursor_update(model);
cursor::queue_lock_changed(model, held.bag, held.slot);
cursor::raise_lock_changed(model);
cursor::raise_cursor_update(model);
}
Some(CursorPayload::Item(held)) => {
// Cloned so the moves below can borrow `model`; an unresolved slot reads as empty.
@@ -360,12 +361,24 @@ fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) {
(Some(_), Some(d)) if d.item_id != held.item_id => {
model.cursor = Some(CursorPayload::Item(held));
}
// Anything else queues the move and clears: a split, a merge the server tops up,
// or a swap that lands the displaced item in the source slot.
(count, _) => {
queue_move(model, &held, bag, slot, count);
cursor::queue_cursor_update(model);
cursor::queue_lock_changed(model, held.bag, held.slot);
// A split places and clears (`0x5e1210`, then `ClearCursor(0,1)` at `0x4f9e4f`):
// the grid hide and `CURSOR_UPDATE`, and no lock event.
(Some(n), _) => {
queue_move(model, &held, bag, slot, Some(n));
cursor::raise_cursor_update(model);
}
// A whole stack locks the destination's item first (`0x4f9e85`; an empty slot's
// zero guid signals nothing, `0x4953ed`), then the swap send clears
// (`ClearCursor(0,1)` at `0x5e0fe7`: the grid hide, `CURSOR_UPDATE`) and the
// binding clears again (`0x4f9eb8`), a second `CURSOR_UPDATE` on the empty cursor.
// The source keeps its lock in both: the clears pass no unlock.
(None, dest) => {
queue_move(model, &held, bag, slot, None);
if dest.is_some() {
cursor::raise_lock_changed(model);
}
cursor::raise_cursor_update(model);
model.raise("CURSOR_UPDATE", Vec::new());
}
}
}
@@ -383,7 +396,7 @@ fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) {
if let Some(entry) = entry {
model.merchant_slot_buys.push((bag, slot, entry));
}
cursor::queue_cursor_update(model);
cursor::raise_cursor_update(model);
}
Some(
other @ (CursorPayload::Spell(_)
@@ -588,10 +601,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
lua.globals().set(
"UseContainerItem",
lua.create_function(|lua, (bag, slot, _rest): (i64, u32, mlua::MultiValue)| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
// Every right-click clears the cursor before it reads the slot (`0x4fa198`): a held
// payload goes back, never placed, and an armed gift wrap is cancelled.
cursor::clear_cursor(&mut model);
// payload goes back, never placed, and an armed gift wrap is cancelled. The clear's
// events run first, as the reference's do before it reads the slot.
super::tick::verb(lua, cursor::clear_cursor);
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
if model.repair_mode {
// Then the item in the slot is repaired (`0x4fa1a6`, `0x4fa1da`).
let occupied = model
@@ -616,8 +630,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
lua.globals().set(
"PickupContainerItem",
lua.create_function(|lua, (bag, slot): (i64, u32)| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
pickup_container_item(&mut model, bag, slot);
super::tick::verb(lua, |model| pickup_container_item(model, bag, slot));
Ok(())
})?,
)?;