From cc1dde91e2fb397dd51b6b3e4804b5465ef39691 Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Fri, 9 Oct 2026 12:39:41 +0100
Subject: [PATCH] MONKEY (r18 A1 B004 streaming, world crate): chunk-cache key,
nearest-first admission, stale-ready latch, emergency ground hold + collider
timeout
World-crate part of benilla-everwood e9247623 (terrain_stream, spawn, wmo_portal).
(cherry picked from commit e9247623, benilla-app hunks omitted)
---
crates/benilla-world/src/terrain_stream.rs | 192 +++++++++++++++++-
.../src/terrain_stream/spawn/mod.rs | 52 +++--
crates/benilla-world/src/wmo_portal/mod.rs | 14 ++
3 files changed, 239 insertions(+), 19 deletions(-)
diff --git a/crates/benilla-world/src/terrain_stream.rs b/crates/benilla-world/src/terrain_stream.rs
index 7d9e9507..c5d5d9a7 100644
--- a/crates/benilla-world/src/terrain_stream.rs
+++ b/crates/benilla-world/src/terrain_stream.rs
@@ -75,6 +75,8 @@ pub struct TerrainStreamer {
map_dir: Option,
/// The focus tile last streamed around, the furnisher's nearest-first key.
focus: (i32, i32),
+ /// The preload window moves at chunk boundaries, including inside the same tile.
+ focus_chunk: Option<(i32, i32)>,
/// The window's `(inner, outer)` half-widths last frame, logged once per change.
reach: Option<(i32, i32)>,
/// The map's WDT tile index: ADT requests wait for it and consult its `MAIN` grid.
@@ -98,6 +100,9 @@ pub struct TerrainStreamer {
const GLOBAL_WMO_UID: u32 = u32::MAX;
impl TerrainStreamer {
+ fn monkey_same_window(&self, window: StreamWindow) -> bool {
+ self.focus_chunk == Some(window.focus) && self.reach == Some((window.inner, window.outer))
+ }
/// `(spawned, requested)` tiles for the debug panel: furnished, and all in the window.
pub fn residency(&self) -> (usize, usize) {
let spawned = self.tiles.values().filter(|t| t.furnished).count();
@@ -405,6 +410,7 @@ impl Plugin for TerrainPlugin {
Update,
(
finish_colliders,
+ monkey_repair_wmo_residency,
stream_terrain,
furnish_tile_cells,
spawn_loaded_placements,
@@ -446,6 +452,13 @@ impl Plugin for TerrainPlugin {
.add_systems(Update, scope_placement_art)
// Ungated, so the density tracker never misses a boot-time config apply.
.add_systems(Update, rescatter_clutter)
+ .add_systems(
+ PostUpdate,
+ monkey_watch_wmo_residency
+ .after(crate::static_gx::MonkeyStaticGxCullSet)
+ .after(bevy::transform::TransformSystems::Propagate)
+ .run_if(crate::schedule::world_is_live),
+ )
.add_systems(
Update,
// After the interior claim: the reference resolves leaf, indoor and names from one
@@ -586,7 +599,7 @@ fn stream_terrain(
let center = focus.resolve(camera.single().ok().map(|c| c.translation));
let window = StreamWindow::at(view.farclip, center[0], center[1]);
let (cx, cy) = window.focus_tile();
- let same_window = state.focus == (cx, cy) && state.reach == Some((window.inner, window.outer));
+ let same_window = state.monkey_same_window(window);
// Skip an unchanged frame only in a paced, complete, presentable world, since the tail it
// bypasses releases a load; even then the undrawn count stays fresh, so a rebake un-skips it.
if same_window
@@ -604,6 +617,7 @@ fn stream_terrain(
return;
}
state.focus = (cx, cy);
+ state.focus_chunk = Some(window.focus);
if state.reach != Some((window.inner, window.outer)) {
state.reach = Some((window.inner, window.outer));
info!(
@@ -726,10 +740,15 @@ fn stream_terrain(
// Spawn loaded tiles within the frame's budget. The material binds the shared light buffer,
// so a new tile is lit and fogged on its first frame.
let tile_deadline = Instant::now() + SPAWN_BUDGET;
- for (&(tx, ty), tile) in state.tiles.iter_mut() {
- if tile.entity.is_some() {
- continue;
- }
+ let mut landed: Vec<_> = state
+ .tiles
+ .iter()
+ .filter(|(_, t)| t.entity.is_none() && tiles.get(&t.handle).is_some())
+ .map(|(&key, _)| key)
+ .collect();
+ landed.sort_unstable_by_key(|&(tx, ty)| ((tx - cx).abs().max((ty - cy).abs()), tx, ty));
+ for (tx, ty) in landed {
+ let tile = state.tiles.get_mut(&(tx, ty)).unwrap();
let Some(adt) = tiles.get(&tile.handle) else {
continue; // not loaded yet, or missing
};
@@ -852,7 +871,7 @@ fn stream_terrain(
}
// The counting below latches off once fully ready (`complete`); a focus on unfurnished
// ground is a new load and re-arms it.
- if !p.focus_resident {
+ if !p.focus_resident || !same_window {
p.complete = false;
}
// So does a snap onto furnished ground, whose buildings may still be arriving; on the edge
@@ -932,6 +951,92 @@ fn tile_drop_disabled() -> bool {
*ON.get_or_init(|| std::env::var_os("WOW_NO_TILE_DROP").is_some())
}
+/// A resident building whose instance was lost must re-enter the ordinary spawn path. Inspect
+/// once per 64 stream ticks; a failed asset has no instance and is deliberately not retried.
+fn monkey_repair_wmo_residency(
+ mut commands: Commands,
+ mut state: ResMut,
+ placements: ResMut,
+ instances: Query<(), With>,
+ mut progress: ResMut,
+ mut tick: Local,
+) {
+ let scan = tick.is_multiple_of(64);
+ *tick = tick.wrapping_add(1);
+ if !scan {
+ return;
+ }
+ let placements = placements.into_inner();
+ for (&key, p) in &mut placements.by_id {
+ let Some(instance) = p.portal_instance else {
+ continue;
+ };
+ if !p.spawned || instances.contains(instance) {
+ continue;
+ }
+ warn!("MONKEY_WMO_RESIDENCY: uid={} instance={instance} lost with refs={}, re-registering building at {:?}",
+ key as u32, p.refs, bevy_to_wow(p.transform.translation));
+ placements.pending_spawns -= p.doodads.iter().filter(|d| !d.spawned).count();
+ for e in p.entities.drain(..) {
+ commands.entity(e).try_despawn();
+ }
+ p.doodads.clear();
+ p.portal_instance = None;
+ p.spawned = false;
+ placements.pending_spawns += 1;
+ state.settled = false;
+ progress.complete = false;
+ progress.scene_ready = false;
+ }
+}
+
+/// Record the three disappearance stages for the two reported buildings, only on a state edge.
+#[allow(clippy::type_complexity)]
+fn monkey_watch_wmo_residency(
+ placements: Res,
+ instances: Query<&crate::wmo_portal::WmoPortalInstance>,
+ gx: Option>,
+ focus: Res,
+ camera: Query<&GlobalTransform, With>,
+ progress: Res,
+ mut last: Local>,
+) {
+ for uid in [71414, 54893] {
+ let placement = placements.by_id.get(&monkey_wmo_placement_key(uid));
+ let instance = placement.and_then(|p| p.portal_instance);
+ let portal = instance.and_then(|e| instances.get(e).ok());
+ let pvs = portal.map_or(0, |i| i.visible.iter().filter(|&&v| v).count());
+ let region = gx
+ .as_deref()
+ .and_then(|g| instance.and_then(|e| g.world.wmos.get(&e)));
+ let retained = region.map_or(0, |r| r.groups.len());
+ let admitted = gx
+ .as_deref()
+ .and_then(|g| {
+ instance.and_then(|e| {
+ g.world
+ .visible_wmos
+ .iter()
+ .find(|(i, _)| *i == e)
+ .map(|(_, s)| s)
+ })
+ })
+ .map_or(0, |s| s.drawn.iter().filter(|&&v| v).count());
+ let facts = (
+ placement.is_some(),
+ portal.is_some(),
+ pvs,
+ retained,
+ admitted,
+ );
+ if last.insert(uid, facts) != Some(facts) {
+ info!("MONKEY_WMO_WATCH: uid={uid} registered={} instance={instance:?} portal={} pvs={pvs} retained={retained} admitted={admitted} body={:?} eye={:?} focus={:?} resident={} presentable={}",
+ facts.0, facts.1, focus.body_pos(), camera.iter().next().map(|c| bevy_to_wow(c.translation())),
+ progress.focus_tile, progress.focus_resident, progress.presentable());
+ }
+ }
+}
+
/// Register one M2 doodad placement, or bump its refcount if already known.
fn register_doodad(
placements: &mut Placements,
@@ -1009,6 +1114,7 @@ fn drop_streamed_world(
staticgx: Option<&mut crate::static_gx::StaticGx>,
activity: &mut StreamActivity,
) {
+ state.focus_chunk = None;
for ((_tx, _ty), t) in state.tiles.drain() {
despawn_tile_owned(commands, &t);
activity.tiles_dropped += 1;
@@ -1307,6 +1413,35 @@ mod focus_tests {
Some(wow_to_bevy([SPAWN_XY.0, SPAWN_XY.1, 100.0]))
}
+ #[test]
+ fn monkey_a_chunk_crossing_inside_one_tile_reopens_the_preload_window() {
+ // Default farclip reaches 13 chunks. Advancing from chunk 2 to 3 reaches tile +1.
+ let before = StreamWindow {
+ focus: (32 * 16 + 2, 44 * 16 + 2),
+ inner: 11,
+ outer: 13,
+ };
+ let after = StreamWindow {
+ focus: (32 * 16 + 3, 44 * 16 + 2),
+ ..before
+ };
+ assert_eq!(before.focus_tile(), after.focus_tile());
+ assert!(!before.wanted_tiles().contains(&(33, 44)));
+ assert!(after.wanted_tiles().contains(&(33, 44)));
+ let state = TerrainStreamer {
+ focus: before.focus_tile(),
+ focus_chunk: Some(before.focus),
+ reach: Some((before.inner, before.outer)),
+ settled: true,
+ ..Default::default()
+ };
+ assert!(state.monkey_same_window(before));
+ assert!(
+ !state.monkey_same_window(after),
+ "same tile must not suppress an incoming row"
+ );
+ }
+
#[test]
fn the_pick_outranks_the_camera_until_the_avatar_exists() {
let idle = ViewFocus::entry(PICK.0, PICK.1);
@@ -1382,6 +1517,51 @@ mod straddler_tests {
}
}
+ #[test]
+ fn monkey_a_resident_wmo_with_a_lost_instance_is_requeued_without_losing_its_refs() {
+ let mut app = App::new();
+ app.init_resource::()
+ .init_resource::()
+ .init_resource::();
+ let missing = app.world_mut().spawn_empty().id();
+ app.world_mut().despawn(missing);
+ let orphan = app.world_mut().spawn_empty().id();
+ let mut p = straddler((0, 0), vec![orphan]);
+ p.model = ModelHandle::Wmo(Default::default());
+ p.portal_instance = Some(missing);
+ p.refs = 3;
+ let key = monkey_wmo_placement_key(71414);
+ app.world_mut()
+ .resource_mut::()
+ .by_id
+ .insert(key, p);
+ app.world_mut().resource_mut::().settled = true;
+ app.world_mut().resource_mut::().complete = true;
+ app.world_mut()
+ .run_system_once(monkey_repair_wmo_residency)
+ .unwrap();
+ let placements = app.world().resource::();
+ let p = &placements.by_id[&key];
+ assert_eq!(p.refs, 3);
+ assert!(!p.spawned && p.portal_instance.is_none());
+ assert_eq!(placements.pending_spawns, 1);
+ assert!(!app.world().resource::().settled);
+ assert!(!app.world().resource::().complete);
+ assert!(app.world().get_entity(orphan).is_err());
+ // A retired missing asset has no recorded instance: don't retry it forever.
+ app.world_mut()
+ .resource_mut::()
+ .by_id
+ .get_mut(&key)
+ .unwrap()
+ .spawned = true;
+ app.world_mut().resource_mut::().pending_spawns = 0;
+ app.world_mut()
+ .run_system_once(monkey_repair_wmo_residency)
+ .unwrap();
+ assert_eq!(app.world().resource::().pending_spawns, 0);
+ }
+
fn run_handoff(world: &mut World, dead: (i32, i32), uids: Vec, merge_on: bool) {
world
.run_system_once(
diff --git a/crates/benilla-world/src/terrain_stream/spawn/mod.rs b/crates/benilla-world/src/terrain_stream/spawn/mod.rs
index ed92ff6c..e6d31573 100644
--- a/crates/benilla-world/src/terrain_stream/spawn/mod.rs
+++ b/crates/benilla-world/src/terrain_stream/spawn/mod.rs
@@ -161,7 +161,15 @@ pub(super) fn spawn_loaded_placements(
// follows the work, so every frame spawns at least one.
let deadline = Instant::now() + SPAWN_BUDGET;
- 'placements: for (&key, p) in by_id.iter_mut() {
+ // Load the buildings nearest the body before scenery at the far edge of the window.
+ let mut due: Vec<_> = by_id
+ .iter()
+ .filter(|(_, p)| !p.spawned || p.doodads.iter().any(|d| !d.spawned))
+ .map(|(&key, p)| (placement_priority(&streamer, p.transform.translation), key))
+ .collect();
+ due.sort_unstable();
+ 'placements: for (_, key) in due {
+ let p = by_id.get_mut(&key).unwrap();
let unique_id = key as u32;
// 1. The model's own geometry, once; a WMO also resolves its doodad props for step 2.
if !p.spawned {
@@ -174,7 +182,10 @@ pub(super) fn spawn_loaded_placements(
// `alliancebrasscannon_flat.m2`, 2026-09-08). Retire it as an empty
// spawn so the accounting completes; the reference draws nothing there
// either.
- if matches!(asset_server.load_state(h), bevy::asset::LoadState::Failed(_)) {
+ if matches!(
+ asset_server.load_state(h),
+ bevy::asset::LoadState::Failed(_)
+ ) {
warn!("placement {unique_id}: its model failed to load — spawning nothing");
p.spawned = true;
*pending_spawns -= 1;
@@ -345,8 +356,13 @@ pub(super) fn spawn_loaded_placements(
let Some(m) = wmos.get(h) else {
// MONKEY (missing model): see the M2 arm — a WMO the client lacks
// retires as an empty spawn rather than pinning the loading screen.
- if matches!(asset_server.load_state(h), bevy::asset::LoadState::Failed(_)) {
- warn!("placement {unique_id}: its WMO failed to load — spawning nothing");
+ if matches!(
+ asset_server.load_state(h),
+ bevy::asset::LoadState::Failed(_)
+ ) {
+ warn!(
+ "placement {unique_id}: its WMO failed to load — spawning nothing"
+ );
p.spawned = true;
*pending_spawns -= 1;
activity.placements_spawned += 1;
@@ -380,13 +396,19 @@ pub(super) fn spawn_loaded_placements(
refs: &m.portal_refs,
slices: &sky_slices,
},
- m.submeshes.iter().zip(m.submesh_group.iter()).map(|(s, g)| {
- (
- *g,
- matches!(s.wmo_batch, Some(benilla_formats::WmoBatchClass::Ext)),
- &s.geometry.positions[..],
- )
- }),
+ m.submeshes
+ .iter()
+ .zip(m.submesh_group.iter())
+ .map(|(s, g)| {
+ (
+ *g,
+ matches!(
+ s.wmo_batch,
+ Some(benilla_formats::WmoBatchClass::Ext)
+ ),
+ &s.geometry.positions[..],
+ )
+ }),
);
let instance = (has_portals || m.wmo_id != 0).then(|| {
commands
@@ -681,7 +703,8 @@ pub(super) fn spawn_loaded_placements(
.unwrap_or_default();
let slices = wmo.map(fx::portal_slices).unwrap_or_default();
// MONKEY (review fixes): the claim list owns its placement even when no MODR names it.
- let claims = wmo.zip(portal_instance)
+ let claims = wmo
+ .zip(portal_instance)
.map(|(w, instance)| fx::PropClaims::new(w, &slices, placement, instance));
for d in &mut p.doodads {
if d.spawned {
@@ -691,7 +714,10 @@ pub(super) fn spawn_loaded_placements(
// MONKEY (missing model): a prop whose M2 the client doesn't ship would hold its
// BUILDING un-"up" (`placements_pending`) for the loading screen's whole life —
// the 2371 s Stormwind hang on `alliancebrasscannon_flat.m2`. Retire the prop.
- if matches!(asset_server.load_state(&d.handle), bevy::asset::LoadState::Failed(_)) {
+ if matches!(
+ asset_server.load_state(&d.handle),
+ bevy::asset::LoadState::Failed(_)
+ ) {
warn!("placement {unique_id}: a WMO prop's model failed to load — skipping it");
d.spawned = true;
*pending_spawns -= 1;
diff --git a/crates/benilla-world/src/wmo_portal/mod.rs b/crates/benilla-world/src/wmo_portal/mod.rs
index 292988cc..83d4bda7 100644
--- a/crates/benilla-world/src/wmo_portal/mod.rs
+++ b/crates/benilla-world/src/wmo_portal/mod.rs
@@ -298,7 +298,11 @@ fn compute_wmo_pvs(
mut camera_fog: ResMut,
mut camera_claim: ResMut,
mut camera_windows: ResMut,
+ mut monkey_last_stall: Local