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 <noreply@anthropic.com>
(cherry picked from commit a55cfa184377f87734a352c6b399adc8833b25eb)
This commit is contained in:
pkuzic
2026-10-09 12:38:42 +01:00
parent 4fda6cad50
commit 10abf2f4db
4 changed files with 175 additions and 34 deletions
+32 -17
View File
@@ -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<u32>,
placements: Vec<PlacementKey>,
liquid: Vec<Entity>,
/// The impassable-chunk wall collider, a body of its own since its audience differs.
wall: Option<Entity>,
@@ -129,11 +129,18 @@ struct TileState {
merged: Vec<Entity>,
}
// 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<u32, Placement>,
/// By placement kind and MDDF/MODF uniqueId.
by_id: HashMap<PlacementKey, Placement>,
/// 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<M2Model> = 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<WmoModel> = 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<u32, Option<(i32, i32)>> = uids
let mut straddlers: HashMap<PlacementKey, Option<(i32, i32)>> = 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::<Vec<_>>(),
merge_on,
);
},
@@ -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::<ViewFocus>().paced = false;
let instance = app.world_mut().spawn_empty().id();
app.world_mut()
.resource_mut::<Placements>()
.by_id
.insert(7, blank_placement(Some(instance)));
app.world_mut().resource_mut::<Placements>().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::<StaticMerge>();
@@ -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::<Placements>();
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::<WmoGroupVis>(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::<ViewFocus>().paced = false;
app.world_mut()
.resource_mut::<Placements>()
.by_id
.insert(9, blank_placement(None));
app.world_mut().resource_mut::<Placements>().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::<StaticMerge>();
@@ -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::<Placements>();
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::<WmoGroupVis>(blob).is_none());
@@ -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::<AssetServer>().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::<Placements>();
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<Placements>| {
release_placement(
&mut commands,
&mut pl,
54418,
&mut StreamActivity::default(),
);
})
.unwrap();
{
let mut pl = app.world_mut().resource_mut::<Placements>();
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<Placements>| {
release_placement(
&mut commands,
&mut pl,
monkey_wmo_placement_key(54418),
&mut StreamActivity::default(),
);
})
.unwrap();
}
let mut pl = app.world_mut().resource_mut::<Placements>();
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<WmoDoodadInst>) -> Placement {
Placement {
model,
@@ -1185,7 +1286,10 @@ mod retire_tests {
{
let mut pl = app.world_mut().resource_mut::<Placements>();
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::<Placements>();
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);
}
@@ -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::<Placements>()
.by_id
.insert(7, blank_placement());
.insert(super::super::monkey_wmo_placement_key(7), blank_placement());
{
let mut welds = app.world_mut().resource_mut::<HullWelds>();
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::<Placements>();
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::<PendingCollider>(owned[0]).is_some());
}