RegisterEvent ignores case; a frame's first resize precedes OnLoad (#1001)

- `RegisterEvent` and `UnregisterEvent` match a name against the client's event table case-insensitively and drop a name that isn't in it (`0x702140`). A new gate checks that every event name the code fires is in the table.
- A visible XML frame resolves its layout at the post-load hook (`0x76a310` → `0x7680e0(1)`). So its first `OnSizeChanged` fires before its `<Frames>`, `OnLoad` and `OnShow`, as in 1.12.1. A hidden frame still waits for its show.
- `pending_events`' doc now says what the queue is: `SignalEvent` (`0x703e50`) runs handlers in place, and the Lua verbs that still queue are a gap.
- #890 was already fixed by #907; two stale comments around it are corrected.

Fixes #959
Fixes #972
Fixes #560
Closes #890

🤖 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 15:08:07 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 720dcf8188
commit 0f505a2ebf
11 changed files with 279 additions and 65 deletions
+32 -8
View File
@@ -1019,8 +1019,21 @@ 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 bindings raise. Deviation: fired at the next tick, not synchronously as in the
/// reference, because a binding runs inside Lua and cannot re-enter handler dispatch.
/// 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.
pub(crate) pending_events: Vec<(String, Vec<ScriptValue>)>,
/// The last cursor position in UI space (logical px, y-up), behind `GetCursorPosition()`.
pub(crate) cursor_pos: (f32, f32),
@@ -2082,24 +2095,35 @@ impl Model {
/// The post-load hook's step (`0x76a2f0`): a visible frame undefers, with its title region and
/// each region it draws (`0x76a310`, `0x76a324`, `0x76a35b`). A hidden one stays deferred.
pub(crate) fn layout_load_end(&mut self, h: FrameHandle) {
/// Whether any owes a resolve now, as [`Self::layout_on_show_frame`]; the caller runs it
/// before the frame's `<Frames>` are built.
pub(crate) fn layout_load_end(&mut self, h: FrameHandle) -> bool {
let Some(f) = self.arena.frame(h) else {
return;
return false;
};
if !f.effective_visible {
return;
return false;
}
let title = f.title_region;
let regions: Vec<RegionHandle> = f.regions.clone();
self.undefer_frame(h);
let mut owed = self.undefer_frame(h);
if let Some(t) = title {
self.undefer_region(t);
owed |= self.undefer_region(t);
}
for rh in regions {
if Some(rh) != title && self.region_draws(rh) {
self.undefer_region(rh);
owed |= self.undefer_region(rh);
}
}
owed || !self.layout_pass.is_empty()
}
/// The layout ids [`Self::layout_load_end`] undeferred: the frame, its title region and the
/// regions it draws.
pub(crate) fn load_end_roots(&self, h: FrameHandle) -> Vec<u32> {
let mut out = self.show_frame_roots(h);
out.extend(self.show_region_roots(h));
out
}
/// The region's own shown bit (`+0xc4`): not hidden, and linked to its owner.