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());
}