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:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user