From fa029754739195537ba540ce6bc08a772c0117cc Mon Sep 17 00:00:00 2001 From: chairmanpow Date: Mon, 12 Oct 2026 17:02:33 +0100 Subject: [PATCH] fix: the attach and mount teardown path uses try_ commands and child-side ChildOf A parent-side add_child panics against an entity a same-frame teardown already despawned; spawn with ChildOf on the child and use try_insert/try_remove/ try_despawn so the same-frame churn warns instead of aborting. --- crates/benilla-app/src/entities/attach/mod.rs | 50 +++++++++++-------- .../src/entities/attach/redress.rs | 4 +- crates/benilla-app/src/entities/mount.rs | 35 +++++++------ 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs index 5979e073..3066290f 100644 --- a/crates/benilla-app/src/entities/attach/mod.rs +++ b/crates/benilla-app/src/entities/attach/mod.rs @@ -115,7 +115,7 @@ fn setup_skinned_instance( commands .entity(entity) .try_insert(rig) - .remove::(); + .try_remove::(); slot } // Nothing to skin: no starvation marker, or `heal_rig_starved` would rebuild the body for a @@ -191,7 +191,7 @@ fn arm_sequence_clock( commands .entity(entity) .try_insert(AnimationTransitions::new()) - .remove::(); + .try_remove::(); } _ => { commands @@ -203,7 +203,7 @@ fn arm_sequence_clock( )) // A rebuilt rig is born live: a stale park marker would desync the LOD gate's // edge-triggered bookkeeping. - .remove::(); + .try_remove::(); } } } @@ -361,9 +361,11 @@ pub(super) fn attach_entity_visuals( } else if let Some(&super::mount::MountChild(child)) = mount_child { // Dismounted while pending: drop the mount it ordered. if let Ok(mut ec) = commands.get_entity(child) { - ec.despawn(); + ec.try_despawn(); } - commands.entity(entity).remove::(); + commands + .entity(entity) + .try_remove::(); } let kind = match net.kind { EntityKind::GameObject => ModelKind::GameObject, @@ -397,7 +399,7 @@ pub(super) fn attach_entity_visuals( _ => { commands .entity(entity) - .remove::(); + .try_remove::(); } } let object = WorldObject { @@ -425,8 +427,11 @@ pub(super) fn attach_entity_visuals( // dispatches on the mount's model (`0x7106c0`) and tilts through its node. let mut joints_root = rider_root; if d.terrain_tilt != 0 && rider_root == entity { + // Child-side `ChildOf`, not `add_child`: a parent-side add panics against a + // unit that a same-frame teardown despawned mid-build. let node = commands .spawn(( + ChildOf(entity), super::conform::ConformNode { // The ground and yaw source: the unit, or a mount child's // host. @@ -437,7 +442,6 @@ pub(super) fn attach_entity_visuals( Visibility::default(), )) .id(); - commands.entity(entity).add_child(node); joints_root = node; } setup_skinned_instance( @@ -663,7 +667,7 @@ pub(super) fn attach_entity_visuals( } else if reattached { commands .entity(entity) - .remove::(); + .try_remove::(); } // Size is `OBJECT_FIELD_SCALE_X` alone: the server already folds the DBC scale into // it (vmangos `Unit::GetScaleForDisplayId`, `Unit.cpp:9579`), and the reference @@ -834,19 +838,21 @@ pub(super) fn attach_entity_visuals( id: net.display_id.unwrap_or(0), detail: String::new(), }; - commands.entity(entity).with_children(|parent| { - parent.spawn(( - Mesh3d(mesh), - MeshMaterial3d(material), - Transform::from_translation(Vec3::Y * lift), - object, - benilla_world::interact::CreaturePickPart, - // No `RenderSubmesh`: the picker casts against the cube's box. - benilla_world::interact::PickBox, - // Counted by `WOW_UNIT_VISUALS`. - super::FallbackCube, - )); - }); + // Child-side `ChildOf`, like every model part in `dress`: the parent-side + // `with_children` would `add_related` against a unit that a same-frame teardown + // already despawned, and panic where the child spawn only warns. + commands.spawn(( + ChildOf(entity), + Mesh3d(mesh), + MeshMaterial3d(material), + Transform::from_translation(Vec3::Y * lift), + object, + benilla_world::interact::CreaturePickPart, + // No `RenderSubmesh`: the picker casts against the cube's box. + benilla_world::interact::PickBox, + // Counted by `WOW_UNIT_VISUALS`. + super::FallbackCube, + )); } } // The mount the visual was built on, for `reseat_mounts` to diff; stamped on a cube too, @@ -864,7 +870,7 @@ pub(super) fn attach_entity_visuals( .try_insert(VisualAttached) // The display the visual was built with, for `refresh_live_display` to diff. .try_insert(super::live_display::AppliedDisplay(net.display_id)) - .remove::(); + .try_remove::(); } } diff --git a/crates/benilla-app/src/entities/attach/redress.rs b/crates/benilla-app/src/entities/attach/redress.rs index 683d359e..a03db3c2 100644 --- a/crates/benilla-app/src/entities/attach/redress.rs +++ b/crates/benilla-app/src/entities/attach/redress.rs @@ -191,10 +191,10 @@ pub(in crate::entities) fn redress_player_looks( // A billboard card is a world root, so it is reaped by name. if let Some(card) = dp.card { if let Ok(mut ec) = commands.get_entity(card) { - ec.despawn(); + ec.try_despawn(); } } - commands.entity(child).despawn(); + commands.entity(child).try_despawn(); hidden += 1; continue; } diff --git a/crates/benilla-app/src/entities/mount.rs b/crates/benilla-app/src/entities/mount.rs index 8f151359..3ede1450 100644 --- a/crates/benilla-app/src/entities/mount.rs +++ b/crates/benilla-app/src/entities/mount.rs @@ -45,8 +45,11 @@ pub(super) fn spawn_mount_child( scale: f32, fade_skip: bool, ) -> Entity { + // Child-side `ChildOf(host)`, not `add_child`: a parent-side add panics against a host that a + // same-frame teardown despawned mid-build. let child = commands .spawn(( + ChildOf(host), NetEntity { kind: EntityKind::Unit, display_id: Some(display), @@ -60,7 +63,6 @@ pub(super) fn spawn_mount_child( if fade_skip { commands.entity(child).insert(super::equipment::Reattached); } - commands.entity(host).add_child(child); // `try_`: the host may be torn down in the same frame this build runs (a battleground's // stream churn), and a plain insert would panic where `despawn` only warns. commands.entity(host).try_insert(MountChild(child)); @@ -79,13 +81,13 @@ fn spawn_seat_anchor( ) -> Entity { let anchor = commands .spawn(( + ChildOf(joint), Transform::from_translation(offset) .with_scale(Vec3::splat(1.0 / mount_scale.max(0.001))), Visibility::default(), benilla_world::rig_anim::RigFrame(rider), )) .id(); - commands.entity(joint).add_child(anchor); anchor } @@ -135,9 +137,9 @@ pub(super) fn seat_or_spawn_mount( let built = children.get(child).map(|(_, _, n)| n.display_id).ok(); if built != Some(Some(mount_display)) { if let Ok(mut ec) = commands.get_entity(child) { - ec.despawn(); + ec.try_despawn(); } - commands.entity(unit).remove::(); + commands.entity(unit).try_remove::(); return Seat::Wait; } let Ok((true, Some(bones), _)) = children.get(child) else { @@ -185,12 +187,12 @@ fn ground_frame( } let node = commands .spawn(( + ChildOf(unit), super::conform::ConformNode { unit, mode: tilt }, Transform::default(), Visibility::default(), )) .id(); - commands.entity(unit).add_child(node); node } @@ -202,15 +204,16 @@ fn reseat_rig(commands: &mut Commands, rig: &mut RigPose, frame: Entity, conform return; } let anchors: Vec = rig.anchors.iter().map(|&(_, a)| a).collect(); - if !anchors.is_empty() { - commands.entity(frame).add_children(&anchors); + for anchor in anchors { + // Child-side, so a frame despawned in the same frame warns rather than panicking. + commands.entity(anchor).try_insert(ChildOf(frame)); } let old = std::mem::replace(&mut rig.joints_root, frame); rig.pose_dirty = true; // A conform node the rig just left is empty, so it goes; a seat anchor dies with its mount. if conform { if let Ok(mut ec) = commands.get_entity(old) { - ec.despawn(); + ec.try_despawn(); } } } @@ -254,11 +257,11 @@ pub(super) fn reseat_mounts( let Ok(seated_root) = poses.get(entity).map(|r| r.joints_root) else { if let Some(&MountChild(child)) = child { if let Ok(mut ec) = commands.get_entity(child) { - ec.despawn(); + ec.try_despawn(); } - commands.entity(entity).remove::(); + commands.entity(entity).try_remove::(); } - commands.entity(entity).insert(AppliedMount(live)); + commands.entity(entity).try_insert(AppliedMount(live)); continue; }; let child = child.map(|&MountChild(c)| c); @@ -276,13 +279,13 @@ pub(super) fn reseat_mounts( } if let Some(child) = child { if let Ok(mut ec) = commands.get_entity(child) { - ec.despawn(); + ec.try_despawn(); } } commands .entity(entity) - .remove::() - .insert(AppliedMount(0)); + .try_remove::() + .try_insert(AppliedMount(0)); continue; } // ── Leg 2 · `0x607a00`: build the mount model and attach the body to its seat. @@ -302,10 +305,10 @@ pub(super) fn reseat_mounts( let leaving_conform = conforms.contains(seated_root); reseat_rig(&mut commands, &mut rig, anchor, leaving_conform); } - commands.entity(entity).insert(AppliedMount(live)); + commands.entity(entity).try_insert(AppliedMount(live)); } Seat::UnitMatrix => { - commands.entity(entity).insert(AppliedMount(live)); + commands.entity(entity).try_insert(AppliedMount(live)); } } }