map: the Continent and Zone pickers were blank on the first map of a session

The realm's `WorldMapFrame.xml` fills them from exactly one place: the Zone
dropdown's `<OnEvent>` 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 `<OnShow>` (:318 —
the realm commented the `OnLoad` out and moved it there), while
`WorldMapFrame`'s own `<OnShow>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 21:57:07 -07:00
co-authored by Claude Opus 5
parent d1382f8e01
commit 5ed9dded4c
2 changed files with 75 additions and 0 deletions
@@ -668,3 +668,58 @@ fn worldmap_overlays_reveal_by_explored_bits() {
s.run("SetMapZoom(2)").unwrap();
assert_eq!(s.eval::<i64>("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 `<OnShow>`, and the two map dropdowns
/// register for the event in *their* `<OnShow>` — 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::<i64>("return seen").unwrap(),
0,
"the inline fire is before the registration, by construction"
);
s.tick(0.0);
assert_eq!(
s.eval::<i64>("return seen").unwrap(),
1,
"the deferred follow-up must reach a handler that registered during the show"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+20
View File
@@ -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 `<OnShow>` runs `SetMapToCurrentZone()`
// (:711) while the two map dropdowns register `WORLD_MAP_UPDATE` in their own
// `<OnShow>` (: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::<Model>().expect("model app_data");
model
.pending_events
.push(("WORLD_MAP_UPDATE".to_string(), Vec::new()));
}
Ok(())
})?,
)?;