From 5ed9dded4c508e52546d27457235950b5aa9fd03 Mon Sep 17 00:00:00 2001 From: grom Date: Mon, 2 Nov 2026 21:57:07 -0700 Subject: [PATCH] map: the Continent and Zone pickers were blank on the first map of a session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The realm's `WorldMapFrame.xml` fills them from exactly one place: the Zone dropdown's `` on `WORLD_MAP_UPDATE` (:321), which re-runs both pickers' initialisers and then the two `WorldMap_Update*DropDownText` functions. That dropdown registers the event in its own `` (:318 — the realm commented the `OnLoad` out and moved it there), while `WorldMapFrame`'s own `` fires it through `SetMapToCurrentZone()` (:711), and a parent's OnShow runs before its children's. So the inline event landed in a gap where its only listener did not exist yet. Measured through the addon harness: the Zone dropdown answers `IsEventRegistered` nil before the open and 1 immediately after, and the pickers come up blank on the first map and correct on the second — which is what grom's recording shows, blank over Dun Morogh and filled over Elwynn later in the same clip. `SetMapToCurrentZone` now queues a second `WORLD_MAP_UPDATE` on the deferred lane as well as firing the inline one. The inline fire stays: it is what repaints the detail tiles inside the OnShow rather than a frame later (the world-map flash). Only this verb doubles up, and only this verb needs to — `SetMapZoom` is a click on a map that is already open. The first attempt at this was reverted and the reason is in the test's own comment: its pin opened the real map on the shipped interface and passed with *and* without the fix, because the stock ordering happens to be fine and the symptom needs the addons. The pin that replaced it asserts the engine half directly — a handler that registers after `SetMapToCurrentZone` returns still receives the event — and answers 0 without the change and 1 with it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../benilla-ui/src/script/tests/worldmap.rs | 55 +++++++++++++++++++ crates/benilla-ui/src/script/worldmap.rs | 20 +++++++ 2 files changed, 75 insertions(+) diff --git a/crates/benilla-ui/src/script/tests/worldmap.rs b/crates/benilla-ui/src/script/tests/worldmap.rs index 9c884167..6073591e 100644 --- a/crates/benilla-ui/src/script/tests/worldmap.rs +++ b/crates/benilla-ui/src/script/tests/worldmap.rs @@ -668,3 +668,58 @@ fn worldmap_overlays_reveal_by_explored_bits() { s.run("SetMapZoom(2)").unwrap(); assert_eq!(s.eval::("return GetNumMapOverlays()").unwrap(), 0); } + +/// `SetMapToCurrentZone` leaves a `WORLD_MAP_UPDATE` behind for a handler that was not registered +/// when it fired. +/// +/// The reference's map calls it from `WorldMapFrame`'s own ``, and the two map dropdowns +/// register for the event in *their* `` — a parent's runs first, so a purely inline event +/// is dispatched into a gap where its only listener does not exist yet. Measured through the addon +/// harness, which is the oracle here because the symptom needs the real `WorldMapFrame.xml`: the +/// Zone dropdown answers `IsEventRegistered` nil before the open and 1 immediately after, and the +/// pickers come up blank on the first map of a session and correct on the second. +/// +/// This pins the engine half of it, which is the part benilla owns: whatever registers late still +/// gets told. A test that opened the real map instead would pass either way — the stock interface's +/// own ordering happens to be fine — and that is exactly how the first attempt at this fix was +/// waved through before the harness caught it out. +#[test] +fn set_map_to_current_zone_also_tells_a_handler_that_registers_during_the_show() { + let mut s = script(); + push_catalog(&mut s); + s.set_world_map_feed( + Some((2, 1)), + Some((0.5, 0.5)), + 0.0, + None, + Vec::new(), + Vec::new(), + ); + // The catalog and feed pushes queue a `WORLD_MAP_UPDATE` of their own ("an open map repaints"), + // so drain the setup before counting — otherwise the probe below sees two and the test is + // measuring the harness. + s.tick(0.0); + s.run( + r#" + seen = 0 + probe = CreateFrame("Frame") + probe:SetScript("OnEvent", function() seen = seen + 1 end) + -- The verb first, the registration after: the order a child's OnShow sees. + SetMapToCurrentZone() + probe:RegisterEvent("WORLD_MAP_UPDATE") + "#, + ) + .unwrap(); + assert_eq!( + s.eval::("return seen").unwrap(), + 0, + "the inline fire is before the registration, by construction" + ); + s.tick(0.0); + assert_eq!( + s.eval::("return seen").unwrap(), + 1, + "the deferred follow-up must reach a handler that registered during the show" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} diff --git a/crates/benilla-ui/src/script/worldmap.rs b/crates/benilla-ui/src/script/worldmap.rs index d869052e..cfd27b8a 100644 --- a/crates/benilla-ui/src/script/worldmap.rs +++ b/crates/benilla-ui/src/script/worldmap.rs @@ -793,6 +793,26 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { select_resolved(&mut model, c, z, direct); } signal_world_map_update(lua); + // …and again on the deferred lane, for the handlers that register *during* the show + // this call is inside. `WorldMapFrame.xml`'s `` runs `SetMapToCurrentZone()` + // (:711) while the two map dropdowns register `WORLD_MAP_UPDATE` in their own + // `` (:286, :318 — the realm commented their `OnLoad` out and moved it there), + // and a parent's OnShow runs before its children's. Measured: the Zone dropdown answers + // `IsEventRegistered` nil before the open and 1 immediately after, so the inline event + // above lands in the gap, and the first map of a session opens with both pickers blank + // and Zoom Out dead — which is what grom's screen recording shows, filled on a later + // open in the same clip. + // + // The inline fire stays: it is what repaints the detail tiles inside the OnShow rather + // than a frame later (the world-map flash, see `signal_world_map_update`). Only this + // verb doubles up, and only this verb needs to — `SetMapZoom` is a click on a map that + // is already open, where every handler registered long ago. + { + let mut model = lua.app_data_mut::().expect("model app_data"); + model + .pending_events + .push(("WORLD_MAP_UPDATE".to_string(), Vec::new())); + } Ok(()) })?, )?;