app: publish the world-map feed from the frame's own area

`SetMapToCurrentZone()` answers out of the `(continent, zone)` pair
`feed_world_map` pushes into the VM, because the VM cannot query the
world — the reference's resolver `0x4a6650` reads the player's area
live. That makes the schedule part of the verb's contract, and only
half of it was declared: the feed ordered `.after(UiInput)` and
nothing else, so it ran before `terrain_stream::update_current_area`
wrote the area under the player and published the previous frame's
zone.

At a crossing the feed therefore named the zone just left, `area`'s
`feed_zone_events` fired `ZONE_CHANGED_NEW_AREA` on the new one, and
both addons that answer it while the map is closed reselected where
the map already was. From grom's 03:57 log at the Ironforge gate:
`ZONE_CHANGED_NEW_AREA 10/10 handler(s) · MINIMAP_ZONE_CHANGED 8/8
handler(s) · the UI asked to follow the player 3× · WorldMapFrame
shown=Some(false)` — every handler reached, the verb called three
times, the selection unmoved. Every map-derived overlay then sits a
zone behind, `GetPlayerMapPosition` and so pfQuest's minimap pins
included, until the player opens the world map by hand (pfQuest
reselects on close), which is the workaround he reported.

Ordered after `AreaAuthoritySet`, whose own doc already required it of
every consumer of the area, and pinned by a schedule test over the
declared graph: remove the edge and it fails naming both systems.

The watchdog's zero-ask arm said "nothing has called
`SetMapToCurrentZone()` since the player crossed" through all of this.
The count was right and the window was not: it starts where this feed
first publishes the new zone, which the stale feed made *later* than
the addons' answer, so three real calls read as none. That one wrong
fork cost two sessions; the wording now names the window it measures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-23 04:22:49 -06:00
co-authored by Claude Opus 5
parent 49b4f7b55e
commit d3d2103919
3 changed files with 107 additions and 10 deletions
+9 -6
View File
@@ -34,12 +34,15 @@
//! unbroken group. The reference cannot be doing that: it splashes "Dun Morogh" on the way out,
//! and `GetZoneText` is how it does so. Chosen on that evidence rather than traced.
//!
//! It is **not** what grom's 2026-10-22 stale-minimap report was: the run after it reproduced the
//! report with the zone-text interior claim reading `outdoors` at the gate, so the skip was never
//! consulted there. What that run did establish is narrower and stranger — the election fires and
//! not one of the handlers registered for it asks to follow the player, with nothing raised. The
//! fire site logs the whole handshake for that reason; `fire_event_reach` and `frame_shown` exist
//! to make its three readings tell apart.
//! It is **not** what grom's 2026-10-22 stale-minimap report was, and nothing on this side was:
//! that run reproduced the report with the zone-text interior claim reading `outdoors` at the
//! gate, so the skip was never consulted. The handshake the fire site logs
//! (`fire_event_reach`, `frame_shown`) is what finally named the cause, in one run: the election
//! fired, every registered handler ran — 10/10 and 8/8 — and the UI asked to follow the player
//! 3× on the spot. The verb was answering out of a feed a frame behind, because
//! `ui_world_map::feed_world_map` was not ordered after the area authority. Fixed there, and
//! pinned by `game_plugins::schedule_tests`; the instrument stays, since it is the only thing
//! that can tell a dropped event from an answered one.
//!
//! The reference's PvP display gate `[0x88272c]` (a cached boolean, not the realm type) is
//! untraced; ours is always open.
+75
View File
@@ -1265,6 +1265,81 @@ pub(crate) mod schedule_tests {
assert!(stale.is_empty(), "stale rows:\n {}", stale.join("\n "));
}
/// `SetMapToCurrentZone()` answers out of a per-frame feed, where the reference's resolver
/// `0x4a6650` reads the player's area live. That one deviation makes the schedule part of the
/// verb's contract, in a chain of three: `update_current_area` writes the area under the
/// player, `feed_world_map` publishes the `(continent, zone)` the verb returns, and
/// `crate::area::feed_zone_events` fires the `ZONE_CHANGED*` an addon answers by calling it.
/// Any two of those in the wrong order and the verb answers for the frame before.
///
/// Asserted rather than commented because the hole cost three play sessions and two wrong
/// diagnoses: only the downstream edge was declared, so the feed published the *previous*
/// frame's leaf, the event fired on the new one, and both addons that answer it reselected
/// the zone the player had just left. Nothing raised, nothing skipped, the verb provably
/// called — and every map-derived overlay a zone behind, `GetPlayerMapPosition` and so
/// pfQuest's minimap pins included, until the player opened the world map by hand.
#[test]
fn the_world_map_feed_sits_between_the_area_authority_and_the_zone_events() {
let mut app = headless_client();
let c = census(&mut app, Update, SyncPoints::Declared);
if !type_names_available(&c) {
eprintln!(
"skipped: this build carries no type names (bevy/debug rides with the dev feature)"
);
return;
}
let one = |suffix: &str| -> SystemKey {
let mut hits = c.systems.iter().filter(|(_, s)| s.name.ends_with(suffix));
match (hits.next(), hits.next()) {
(Some((k, _)), None) => *k,
_ => panic!("exactly one system named `…{suffix}` in Update"),
}
};
let area = one("::queries::update_current_area");
let feed = one("::ui_world_map::feed_world_map");
let events = one("::area::feed_zone_events");
let mut pred: HashMap<SystemKey, Vec<SystemKey>> = HashMap::new();
for (a, b) in &c.dependencies {
pred.entry(*b).or_default().push(*a);
}
let runs_before = |a: SystemKey, b: SystemKey| {
let mut seen = HashSet::new();
let mut stack = vec![b];
while let Some(n) = stack.pop() {
for m in pred.get(&n).into_iter().flatten() {
if *m == a {
return true;
}
if seen.insert(*m) {
stack.push(*m);
}
}
}
false
};
for (a, b, why) in [
(
area,
feed,
"the feed would publish the previous frame's zone, so the verb answers for the \
zone the player just left",
),
(
feed,
events,
"the event would fire before the zone it reports reaches the verb an addon \
answers it with",
),
] {
assert!(
runs_before(a, b),
"`{}` must be declared before `{}` — otherwise {why}",
c.name(a),
c.name(b)
);
}
}
/// The consumer markers: a VM holder that does one of these to host state has spent
/// something this frame that nothing can re-spend.
const CONSUMES: &[&str] = &["fire_event(", "mem::take(", ".drain("];
+23 -4
View File
@@ -1001,10 +1001,16 @@ fn feed_world_map(
Some(LagReport::Stuck { age, asks }) => {
// The two halves have the same symptom and different fixes, so the line names
// which one this is rather than leaving it to be guessed from a later session.
// The count is taken from the frame this feed first published the new zone, not
// from the crossing, and saying "since the player crossed" cost a diagnosis: the
// feed used to run before the area authority, so an addon's reselection landed a
// frame *earlier* than the baseline and read as never having happened. The feed
// now orders after it (`WorldMapFeedSet`), which is what makes the two windows
// the same one; the wording stays exact anyway.
let why = if asks == 0 {
"nothing has called `SetMapToCurrentZone()` since the player crossed, so the \
`ZONE_CHANGED*` an addon answers with it never reached one (pfQuest answers \
all three while the map is closed, `pfQuest/map.lua:1466`)"
"nothing has called `SetMapToCurrentZone()` since this feed published the new \
zone, so the `ZONE_CHANGED*` an addon answers with it never reached one \
(pfQuest answers all three while the map is closed, `pfQuest/map.lua:1466`)"
.to_string()
} else {
format!(
@@ -1209,6 +1215,14 @@ fn dev_map_jump(
/// (`benilla_ui::script::worldmap`'s `player_zone`) and a stale read reselects the zone the player
/// just left. In the reference there is nothing to order: `SetMapToCurrentZone` calls the resolver
/// `0x4a6650`, which reads the player's current area live, where ours reads a per-frame feed.
///
/// The same reason gives the set its own upstream edge, on
/// [`benilla_world::terrain_stream::AreaAuthoritySet`], which is where the area under the player
/// is written. Ordering only the downstream half is worth nothing: this feed read the *previous*
/// frame's leaf, `crate::area` fired `ZONE_CHANGED_NEW_AREA` on the new one, and the addons
/// answered against a `player_zone` still naming the zone behind them — the verb called, the
/// selection reselected where it already was, and nothing in any log said a thing but the
/// watchdog below. That is what grom's three stale-minimap reports were.
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct WorldMapFeedSet;
@@ -1221,7 +1235,12 @@ impl Plugin for WorldMapUiPlugin {
Update,
(
// After the script tick: a selection changed this tick projects on the next.
feed_world_map.after(UiInput).in_set(WorldMapFeedSet),
// After the area authority: the verb this feed backs must answer for the frame
// it is called in, not the one before (see `WorldMapFeedSet`).
feed_world_map
.after(UiInput)
.after(benilla_world::terrain_stream::AreaAuthoritySet)
.in_set(WorldMapFeedSet),
// After the script tick, so the jump hit-tests this tick's frame rects.
dev_map_jump.after(UiInput),
),