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
+11 -15
View File
@@ -1019,22 +1019,17 @@ pub(crate) struct Model {
pub(crate) chat_bubbles: super::chat_bubble::ChatBubbles,
/// The world-state readout's rows, gated and resolved by the app.
pub(crate) worldstate: super::worldstate::WorldStateUiState,
/// Events raised outside a dispatch, fired at the start of the next tick ahead of its `OnUpdate`
/// pass (`UiScript::tick`). The reference queues nothing here: a fire site calls
/// `SignalEvent` (`0x703e50`, `0x703f50`), which runs every handler before it returns, so a
/// verb's event reaches its listeners inside the call.
///
/// A binding can do the same: `tick::fire_event_into` re-enters dispatch from inside Lua, as
/// `SetCVar`'s `CVAR_UPDATE` does, once the binding holds no model borrow. So the queue is no
/// limit, and a Lua verb that pushes here instead is a gap against the reference: the cursor
/// verbs' `CURSOR_UPDATE`, grid, `ITEM_LOCK_CHANGED`, `DELETE_ITEM_CONFIRM` and
/// `PLAYER_MONEY`, the send-mail verbs' events, `SetGuildRecruitmentMode`'s
/// `UPDATE_CHAT_WINDOWS`, `SetBattlefieldScoreFaction`'s `UPDATE_BATTLEFIELD_SCORE`,
/// `UpdateInventoryAlertStatus`'s `UPDATE_INVENTORY_ALERTS`, `CancelAreaSpiritHeal`'s
/// `AREA_SPIRIT_HEALER_OUT_OF_RANGE` and the trainer verbs' `TRAINER_UPDATE`. The host's own
/// setters (the world map's, the world-state rows, a dropped mail attachment) and
/// `UiScript::queue_event` have no verb to fire inside and queue by design.
/// The host's event queue, fired at the start of the next tick ahead of its `OnUpdate` pass
/// (`UiScript::tick`). Host producers only: the app's setters (the world map's, the
/// world-state rows, a dropped mail attachment), `UiScript::queue_event`, and an engine path
/// such as a world click that clears the cursor, which has no verb to fire inside and queues
/// so it stays behind the input already queued. A Lua verb never lands here: the reference's
/// `SignalEvent` (`0x703e50`, `0x703f50`) runs every handler before it returns, so a verb's
/// events fire inside the call, through [`super::tick::verb`].
pub(crate) pending_events: Vec<(String, Vec<ScriptValue>)>,
/// The events the running [`super::tick::verb`] segment has raised, fired in order once its
/// model borrow drops; `None` outside a segment, where [`Model::raise`] queues instead.
pub(crate) verb_events: Option<Vec<(String, Vec<ScriptValue>)>>,
/// The last cursor position in UI space (logical px, y-up), behind `GetCursorPosition()`.
pub(crate) cursor_pos: (f32, f32),
@@ -1543,6 +1538,7 @@ impl Model {
chat_bubbles: super::chat_bubble::ChatBubbles::default(),
worldstate: super::worldstate::WorldStateUiState::default(),
pending_events: Vec::new(),
verb_events: None,
cursor_pos: (0.0, 0.0),
minimap_ping_request: None,
minimap_ping: (0.0, 0.0),