From 10abf2f4dbb60d526b5cd402b670e1591ac24c68 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Thu, 8 Oct 2026 13:32:40 +0100 Subject: [PATCH] MONKEY (r16 E3): separate MDDF/MODF placement namespaces so same-UID M2 and WMO both stream Placements.by_id was keyed by the raw 32-bit UID, so an M2 (MDDF) and a WMO (MODF) carrying the same numeric UID merged: the second registration only bumped the first one's refcount and never spawned its model (approach-order dependent; e.g. UID 54418 farm.wmo vs redridgefallentree01, 54705 humantwostory.wmo vs elwynnbush09, 55333 redridgedocks02.wmo vs elwynnbush09). Key is now u64: M2 = uid, WMO = (1<<32)|uid, carried through tile refs, refcount/release, WMO readiness, straddler bookkeeping, prop merge and collider weld. Spawn metadata keeps the 32-bit UID. Regression: monkey_mddf_and_modf_with_the_same_uid_stream_independently. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit a55cfa184377f87734a352c6b399adc8833b25eb) --- crates/benilla-world/src/terrain_stream.rs | 49 +++++--- .../benilla-world/src/terrain_stream/merge.rs | 35 ++++-- .../src/terrain_stream/spawn/mod.rs | 112 +++++++++++++++++- .../benilla-world/src/terrain_stream/weld.rs | 13 +- 4 files changed, 175 insertions(+), 34 deletions(-) diff --git a/crates/benilla-world/src/terrain_stream.rs b/crates/benilla-world/src/terrain_stream.rs index 0c2c41c4..7d9e9507 100644 --- a/crates/benilla-world/src/terrain_stream.rs +++ b/crates/benilla-world/src/terrain_stream.rs @@ -117,7 +117,7 @@ struct TileState { /// Every cell is up: the residency bit the loading screen counts. furnished: bool, /// The uniqueIds of the placements this tile references, released when it unloads. - placements: Vec, + placements: Vec, liquid: Vec, /// The impassable-chunk wall collider, a body of its own since its audience differs. wall: Option, @@ -129,11 +129,18 @@ struct TileState { merged: Vec, } +// MDDF and MODF use independent IDs in the installed ADTs; retain the kind through release. +type PlacementKey = u64; + +fn monkey_wmo_placement_key(uid: u32) -> PlacementKey { + (1_u64 << 32) | u64::from(uid) +} + /// Doodad and WMO placements, spawned once and refcounted across the tiles that list them. #[derive(Resource, Default)] pub(crate) struct Placements { - /// By MDDF/MODF uniqueId. - by_id: HashMap, + /// By placement kind and MDDF/MODF uniqueId. + by_id: HashMap, /// The material dedup, so submeshes sharing a look share one handle and batch. materials: MaterialCache, /// Placements awaiting their model plus WMO props awaiting their M2s; at zero the spawner skips @@ -777,11 +784,11 @@ fn stream_terrain( // since a doodad's origin may lie on another tile. for d in &adt.doodads { register_doodad(placements, &asset_server, d, (tx, ty)); - tile.placements.push(d.unique_id); + tile.placements.push(u64::from(d.unique_id)); } for w in &adt.wmos { register_wmo(placements, &asset_server, w); - tile.placements.push(w.unique_id); + tile.placements.push(monkey_wmo_placement_key(w.unique_id)); } let mut liquid_ents = Vec::new(); @@ -840,7 +847,7 @@ fn stream_terrain( if state.global_wmo { p.focus_resident &= placements .by_id - .get(&GLOBAL_WMO_UID) + .get(&monkey_wmo_placement_key(GLOBAL_WMO_UID)) .is_some_and(|p| p.spawned); } // The counting below latches off once fully ready (`complete`); a focus on unfurnished @@ -891,7 +898,7 @@ fn stream_terrain( if state.global_wmo { let full = placements .by_id - .get(&GLOBAL_WMO_UID) + .get(&monkey_wmo_placement_key(GLOBAL_WMO_UID)) .is_some_and(|p| p.spawned && p.doodads.iter().all(|d| d.spawned)); p.total += 1; p.ready += usize::from(full); @@ -932,13 +939,13 @@ fn register_doodad( d: &Doodad, tile: (i32, i32), ) { - if let Some(p) = placements.by_id.get_mut(&d.unique_id) { + if let Some(p) = placements.by_id.get_mut(&u64::from(d.unique_id)) { p.refs += 1; return; } let handle: Handle = asset_server.load(m2_url(&d.model)); placements.by_id.insert( - d.unique_id, + u64::from(d.unique_id), Placement { model: ModelHandle::M2(handle), transform: Transform { @@ -961,13 +968,16 @@ fn register_doodad( /// Register one WMO placement, or bump its refcount; a 1.12 WMO placement has no scale. fn register_wmo(placements: &mut Placements, asset_server: &AssetServer, w: &WmoInstance) { - if let Some(p) = placements.by_id.get_mut(&w.unique_id) { + if let Some(p) = placements + .by_id + .get_mut(&monkey_wmo_placement_key(w.unique_id)) + { p.refs += 1; return; } let handle: Handle = asset_server.load(wmo_url(&w.model)); placements.by_id.insert( - w.unique_id, + monkey_wmo_placement_key(w.unique_id), Placement { model: ModelHandle::Wmo(handle), transform: Transform { @@ -1007,7 +1017,12 @@ fn drop_streamed_world( } } if std::mem::take(&mut state.global_wmo) { - release_placement(commands, placements, GLOBAL_WMO_UID, activity); + release_placement( + commands, + placements, + monkey_wmo_placement_key(GLOBAL_WMO_UID), + activity, + ); } placements.materials.clear(); // Cleared here, not by the flush's dead-key discard: tile keys repeat across maps, and the @@ -1159,14 +1174,14 @@ fn handoff_straddlers( tiles: &HashMap<(i32, i32), TileState>, placements: &mut Placements, dead: (i32, i32), - uids: &[u32], + uids: &[PlacementKey], merge_on: bool, ) { if !merge_on { return; } // The dead tile's M2 placements with refs left, then one pass over each neighbour's list. - let mut straddlers: HashMap> = uids + let mut straddlers: HashMap> = uids .iter() .copied() .filter(|uid| { @@ -1243,7 +1258,7 @@ fn handoff_straddlers( fn release_placement( commands: &mut Commands, placements: &mut Placements, - uid: u32, + uid: PlacementKey, activity: &mut StreamActivity, ) { let drop_it = match placements.by_id.get_mut(&uid) { @@ -1343,7 +1358,7 @@ mod straddler_tests { material: None, next_cell: 0, furnished: false, - placements: uids.to_vec(), + placements: uids.iter().copied().map(u64::from).collect(), liquid: Vec::new(), wall: None, clutter: Vec::new(), @@ -1378,7 +1393,7 @@ mod straddler_tests { &streamer.tiles, &mut placements, dead, - &uids, + &uids.iter().copied().map(u64::from).collect::>(), merge_on, ); }, diff --git a/crates/benilla-world/src/terrain_stream/merge.rs b/crates/benilla-world/src/terrain_stream/merge.rs index 5233dae1..5501db40 100644 --- a/crates/benilla-world/src/terrain_stream/merge.rs +++ b/crates/benilla-world/src/terrain_stream/merge.rs @@ -348,7 +348,10 @@ pub(super) fn flush_static_merge( }); } merge.props.retain(|key, acc| { - let Some(p) = placements.by_id.get_mut(&key.0) else { + let Some(p) = placements + .by_id + .get_mut(&super::monkey_wmo_placement_key(key.0)) + else { return false; }; if !acc.ready(frame, idle) { @@ -753,10 +756,10 @@ mod tests { let mut app = test_app(); app.world_mut().resource_mut::().paced = false; let instance = app.world_mut().spawn_empty().id(); - app.world_mut() - .resource_mut::() - .by_id - .insert(7, blank_placement(Some(instance))); + app.world_mut().resource_mut::().by_id.insert( + super::super::monkey_wmo_placement_key(7), + blank_placement(Some(instance)), + ); let rooms: Arc<[u16]> = Arc::from([3u16, 5].as_slice()); { let mut merge = app.world_mut().resource_mut::(); @@ -782,7 +785,12 @@ mod tests { app.world_mut().run_system_once(flush_static_merge).unwrap(); app.world_mut().run_system_once(flush_static_merge).unwrap(); let placements = app.world().resource::(); - let owned = placements.by_id.get(&7).unwrap().entities.clone(); + let owned = placements + .by_id + .get(&super::super::monkey_wmo_placement_key(7)) + .unwrap() + .entities + .clone(); assert_eq!(owned.len(), 1); let blob = owned[0]; let vis = app.world().get::(blob).unwrap(); @@ -808,10 +816,10 @@ mod tests { fn exterior_and_unnamed_prop_blobs_stay_plain() { let mut app = test_app(); app.world_mut().resource_mut::().paced = false; - app.world_mut() - .resource_mut::() - .by_id - .insert(9, blank_placement(None)); + app.world_mut().resource_mut::().by_id.insert( + super::super::monkey_wmo_placement_key(9), + blank_placement(None), + ); let rooms: Arc<[u16]> = Arc::from([].as_slice()); { let mut merge = app.world_mut().resource_mut::(); @@ -835,7 +843,12 @@ mod tests { app.world_mut().run_system_once(flush_static_merge).unwrap(); app.world_mut().run_system_once(flush_static_merge).unwrap(); let placements = app.world().resource::(); - let owned = placements.by_id.get(&9).unwrap().entities.clone(); + let owned = placements + .by_id + .get(&super::super::monkey_wmo_placement_key(9)) + .unwrap() + .entities + .clone(); assert_eq!(owned.len(), 1); let blob = owned[0]; assert!(app.world().get::(blob).is_none()); diff --git a/crates/benilla-world/src/terrain_stream/spawn/mod.rs b/crates/benilla-world/src/terrain_stream/spawn/mod.rs index 085d013b..ed92ff6c 100644 --- a/crates/benilla-world/src/terrain_stream/spawn/mod.rs +++ b/crates/benilla-world/src/terrain_stream/spawn/mod.rs @@ -161,7 +161,8 @@ pub(super) fn spawn_loaded_placements( // follows the work, so every frame spawns at least one. let deadline = Instant::now() + SPAWN_BUDGET; - 'placements: for (&unique_id, p) in by_id.iter_mut() { + 'placements: for (&key, p) in by_id.iter_mut() { + 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 { let entities = match &p.model { @@ -1135,6 +1136,106 @@ mod retire_tests { app } + #[test] + fn monkey_mddf_and_modf_with_the_same_uid_stream_independently() { + use super::super::{ + monkey_wmo_placement_key, register_doodad, register_wmo, release_placement, + StreamActivity, + }; + use benilla_formats::{Doodad, WmoInstance}; + use bevy::ecs::system::RunSystemOnce; + + for wmo_first in [false, true] { + let mut app = app(); + let server = app.world().resource::().clone(); + let tree = Doodad { + model: "World/test/tree.m2".into(), + position: [0.0; 3], + rotation: [0.0; 3], + scale: 1.0, + unique_id: 54418, + }; + let house = WmoInstance { + model: "World/test/farm.wmo".into(), + position: [1.0; 3], + rotation: [0.0; 3], + unique_id: 54418, + doodad_set: 2, + name_set: 0, + }; + { + let mut pl = app.world_mut().resource_mut::(); + if wmo_first { + register_wmo(&mut pl, &server, &house); + } + register_doodad(&mut pl, &server, &tree, (33, 48)); + if !wmo_first { + register_wmo(&mut pl, &server, &house); + } + register_wmo(&mut pl, &server, &house); + assert_eq!( + pl.by_id.len(), + 2, + "one ADT class must not suppress the other" + ); + assert!(matches!(pl.by_id[&54418].model, ModelHandle::M2(_))); + assert!(matches!( + pl.by_id[&monkey_wmo_placement_key(54418)].model, + ModelHandle::Wmo(_) + )); + assert_eq!(pl.by_id[&54418].refs, 1); + assert_eq!(pl.by_id[&monkey_wmo_placement_key(54418)].refs, 2); + assert_eq!(pl.pending_spawns, 2); + } + app.world_mut() + .run_system_once(|mut commands: Commands, mut pl: ResMut| { + release_placement( + &mut commands, + &mut pl, + 54418, + &mut StreamActivity::default(), + ); + }) + .unwrap(); + { + let mut pl = app.world_mut().resource_mut::(); + assert_eq!(pl.by_id.len(), 1); + assert_eq!(pl.by_id[&monkey_wmo_placement_key(54418)].refs, 2); + assert_eq!(pl.pending_spawns, 1); + register_doodad(&mut pl, &server, &tree, (33, 48)); + assert_eq!( + pl.by_id.len(), + 2, + "returning M2 must not merge into the resident WMO" + ); + assert_eq!(pl.pending_spawns, 2); + } + for _ in 0..2 { + app.world_mut() + .run_system_once(|mut commands: Commands, mut pl: ResMut| { + release_placement( + &mut commands, + &mut pl, + monkey_wmo_placement_key(54418), + &mut StreamActivity::default(), + ); + }) + .unwrap(); + } + let mut pl = app.world_mut().resource_mut::(); + assert_eq!(pl.by_id.len(), 1); + assert_eq!(pl.by_id[&54418].refs, 1); + assert_eq!(pl.pending_spawns, 1); + register_wmo(&mut pl, &server, &house); + assert_eq!( + pl.by_id.len(), + 2, + "returning WMO must not merge into the M2" + ); + assert_eq!(pl.pending_spawns, 2); + } + } + fn placement(model: ModelHandle, doodads: Vec) -> Placement { Placement { model, @@ -1185,7 +1286,10 @@ mod retire_tests { { let mut pl = app.world_mut().resource_mut::(); pl.by_id.insert(1, placement(ModelHandle::M2(m2), vec![])); - pl.by_id.insert(2, placement(ModelHandle::Wmo(wmo), vec![])); + pl.by_id.insert( + super::super::monkey_wmo_placement_key(2), + placement(ModelHandle::Wmo(wmo), vec![]), + ); // A handle that was never loaded is not a failure: it keeps waiting. pl.by_id .insert(3, placement(ModelHandle::M2(loading), vec![])); @@ -1193,7 +1297,9 @@ mod retire_tests { } settle(&mut app, &ids); let pl = app.world().resource::(); - assert!(pl.by_id[&1].spawned && pl.by_id[&2].spawned); + assert!( + pl.by_id[&1].spawned && pl.by_id[&super::super::monkey_wmo_placement_key(2)].spawned + ); assert!(!pl.by_id[&3].spawned); assert_eq!(pl.pending_spawns, 1); } diff --git a/crates/benilla-world/src/terrain_stream/weld.rs b/crates/benilla-world/src/terrain_stream/weld.rs index 8e9883b9..6f712057 100644 --- a/crates/benilla-world/src/terrain_stream/weld.rs +++ b/crates/benilla-world/src/terrain_stream/weld.rs @@ -135,7 +135,10 @@ pub(super) fn flush_hull_welds( false }); welds.props.retain(|&uid, acc| { - let Some(p) = placements.by_id.get_mut(&uid) else { + let Some(p) = placements + .by_id + .get_mut(&super::monkey_wmo_placement_key(uid)) + else { return false; }; if !acc.ready(frame, idle) { @@ -298,7 +301,7 @@ mod tests { app.world_mut() .resource_mut::() .by_id - .insert(7, blank_placement()); + .insert(super::super::monkey_wmo_placement_key(7), blank_placement()); { let mut welds = app.world_mut().resource_mut::(); for _ in 0..WELD_MAX_HULLS { @@ -308,7 +311,11 @@ mod tests { } app.world_mut().run_system_once(flush_hull_welds).unwrap(); let placements = app.world().resource::(); - let owned = &placements.by_id.get(&7).unwrap().entities; + let owned = &placements + .by_id + .get(&super::super::monkey_wmo_placement_key(7)) + .unwrap() + .entities; assert_eq!(owned.len(), 1); assert!(app.world().get::(owned[0]).is_some()); }